для самостійної роботи з...

193
1 Коледж Кременчуцького Національного університету імені Михайла Остроградського МЕТОДИЧНИЙ ПОСІБНИК для самостійної роботи з дисципліни «СИСТЕМНЕ ПРОГРАМУВАННЯ» для студентів, які навчаються за спеціальністю 5.05010201 «Обслуговування комп’ютерних систем і мереж» (номер, назва спеціальності) Відділення комп’ютерних мереж та електропобутової техніки Кременчук 2017р.

Upload: others

Post on 07-Jul-2020

9 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

1

Коледж Кременчуцького Національного університету

імені Михайла Остроградського

МЕТОДИЧНИЙ ПОСІБНИК

для самостійної роботи з дисципліни

«СИСТЕМНЕ ПРОГРАМУВАННЯ»

для студентів, які навчаються за спеціальністю

5.05010201 «Обслуговування комп’ютерних систем і мереж» (номер, назва спеціальності)

Відділення комп’ютерних мереж та електропобутової техніки

Кременчук 2017р.

Page 2: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

2

Методичний посібник для самостійної роботи з дисципліни

«СИСТЕМНЕ ПРОГРАМУВАННЯ»

для студентів., які навчаються за спеціальністю

5.05010201 «Обслуговування комп’ютерних систем і мереж» (номер, назва спеціальності)

Відділення комп’ютерних мереж та електропобутової техніки

Укладач: Шинкаренко Л.М. (прізвище викладача)

Розглянуто цикловою комісією з комп’ютерної техніки

Протокол №__________ від «___»_______________ 20__ р.

Голова циклової комісії___________________С.І. Почтовюк

Затверджено методичною радою коледжу

Протокол № ____від «___»________________________р.

Голова методичної ради Левченко Р.В

Page 3: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

3

Зміст.

ВСТУП. МЕТА ТА ЗАДАЧІ ПРЕДМЕТУ. СИСТЕМНЕ ПРОГРАМУВАННЯ. СИСТЕМНЕ ПРОГРАМНЕ

ЗАБЕЗПЕЧЕННЯ. ЕТАПИ СТВОРЕННЯ ПРОГРАМ. .............................................................................................. 5

ОСНОВНІ РОЗДІЛИ ТЕМИ. .................................................................................................................................................. 5 ПИТАННЯ ДЛЯ САМОКОНТРОЛЮ: ..................................................................................................................................... 5

ЗМІСТОВНИЙ МОДУЛЬ 1. ВКАЗІВНИКИ ТА ПОСИЛАННЯ. ДИНАМІЧНИЙ РОЗПОДІЛ ПАМ'ЯТІ.

ДИНАМІЧНІ МАСИВИ. ................................................................................................................................................... 11

ОСНОВНІ РОЗДІЛИ ТЕМИ. ................................................................................................................................................ 11 ПИТАННЯ ДЛЯ САМОКОНТРОЛЮ. ................................................................................................................................... 12

ЗМІСТОВИЙ МОДУЛЬ 2. РЯДКИ. ДИНАМІЧНІ РЯДКИ. ФАЙЛИ. ...................................................................... 24

ПИТАННЯ ДЛЯ САМОКОНТРОЛЮ. ................................................................................................................................... 24 ПИТАННЯ ДЛЯ САМОПЕРЕВІРКИ. .................................................................................................................................... 25

ЗМІСТОВИЙ МОДУЛЬ 3. СТРУКТУРИ. ФАЙЛИ. ТАБЛИЦІ ІДЕНТИФІКАТОРІВ. ............................................. 36

ОСНОВНІ РОЗДІЛИ ТЕМИ. ................................................................................................................................................ 36 ПИТАННЯ ДЛЯ САМОКОНТРОЛЮ. ................................................................................................................................... 36

ЗМІСТОВНИЙ МОДУЛЬ 4. БАГАТОФАЙЛОВІ ПРОЕКТИ. ТЕХНОЛОГІЯ СТВОРЕННЯ ПРОГРАМ.

ДИНАМІЧНІ СТРУКТУРИ ДАНИХ............................................................................................................................ 58

ОСНОВНІ РОЗДІЛИ ТЕМИ. ................................................................................................................................................ 58 ПИТАННЯ ДЛЯ САМОКОНТРОЛЮ. ................................................................................................................................... 58

ЗМІСТОВНИЙ МОДУЛЬ 5. ОБ’ЄКТНО – ОРІЄНТОВАНЕ ПРОГРАМУВАННЯ. КЛАСИ.

КОНСТРУКТОРИ, ДЕСТРУКТОРИ. .......................................................................................................................... 86

ОСНОВНІ РОЗДІЛИ ТЕМИ. ................................................................................................................................................ 86 ПИТАННЯ ДЛЯ САМОПЕРЕВІРКИ. .................................................................................................................................... 86

ЗМІСТОВИЙ МОДУЛЬ 6. РОЗШИРЕНІ МОЖЛИВОСТІ С++. .......................................................................... 115

ОСНОВНІ РОЗДІЛИ ТЕМИ. .............................................................................................................................................. 115 ПИТАННЯ ДЛЯ САМОПЕРЕВІРКИ. .................................................................................................................................. 115

ЛАБОРАТОРНА РОБОТА №1 .................................................................................................................................... 154

ВВЕДЕННЯ В ПРОГРАМУВАННЯ ЗА ДОПОМОГОЮ API ............................................................................... 154

ОСНОВНІ ТЕОРЕТИЧНІ ВІДОМОСТІ ................................................................................................................................ 154 ПОРЯДОК ВИКОНАННЯ РОБОТИ ......................................................................................................................... 156 КОНТРОЛЬНІ ЗАПИТАННЯ .................................................................................................................................... 159

ЛАБОРАТОРНА РОБОТА №2 .................................................................................................................................... 160

СТРУКТУРИ ДАНИХ ДЛЯ ВИКОРИСТАННЯ СИСТЕМНИХ ФУНКЦІЙ ..................................................... 160

ОСНОВНІ ТЕОРЕТИЧНІ ВІДОМОСТІ ................................................................................................................................ 160 ПОРЯДОК ВИКОНАННЯ РОБОТИ ......................................................................................................................... 161 КОНТРОЛЬНІ ЗАПИТАННЯ .................................................................................................................................... 164

ЛАБОРАТОРНА РОБОТА № 3 ................................................................................................................................... 165

РОБОТА З ПАМ'ЯТТЮ ............................................................................................................................................... 165

ОСНОВНІ ТЕОРЕТИЧНІ ВІДОМОСТІ ................................................................................................................................ 165 ПОРЯДОК ВИКОНАННЯ РОБОТИ ......................................................................................................................... 166 КОНТРОЛЬНІ ЗАПИТАННЯ .................................................................................................................................... 170

ЛАБОРАТОРНА РОБОТА № 4 ................................................................................................................................... 171

CТАНДАРТНІ КЛАСИ ВІКОН ТА ЇХ ТИПИ .......................................................................................................... 171

ОСНОВНІ ТЕОРЕТИЧНІ ВІДОМОСТІ ................................................................................................................................ 171 ПОРЯДОК ВИКОНАННЯ РОБОТИ ......................................................................................................................... 174 КОНТРОЛЬНІ ЗАПИТАННЯ .................................................................................................................................... 175

ЛАБОРАТОРНА РОБОТА № 5 ................................................................................................................................... 175

ДОЧІРНІ ВІКНА: ЇХ УТВОРЕННЯ ТА ВЗАЄМОДІЯ, ГРАФІЧНИЙ КОНТЕКСТ. ........................................ 175

Page 4: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

4

ОСНОВНІ ТЕОРЕТИЧНІ ВІДОМОСТІ ................................................................................................................................ 175 ПОРЯДОК ВИКОНАННЯ РОБОТИ ......................................................................................................................... 179 КОНТРОЛЬНІ ЗАПИТАННЯ .................................................................................................................................... 180

ЛАБОРАТОРНА РОБОТА № 6 ................................................................................................................................... 180

СПЕЦІАЛІЗОВАНІ КАТАЛОГИ WINDOWS .......................................................................................................... 180

ОСНОВНІ ТЕОРЕТИЧНІ ВІДОМОСТІ ................................................................................................................................ 180 ПОРЯДОК ВИКОНАННЯ РОБОТИ ......................................................................................................................... 183 КОНТРОЛЬНІ ЗАПИТАННЯ .................................................................................................................................... 184

ЛАБОРАТОРНА РОБОТА № 7 ................................................................................................................................... 184

ЗВОРОТНІЙ ВИКЛИК ТА ФУНКЦІЇ ПЕРЕБОРУ СИСТЕМНИХ ОБ'ЄКТІВ ................................................ 184

ОСНОВНІ ТЕОРЕТИЧНІ ВІДОМОСТІ ................................................................................................................................ 184 ПОРЯДОК ВИКОНАННЯ РОБОТИ ......................................................................................................................... 188 КОНТРОЛЬНІ ЗАПИТАННЯ .................................................................................................................................... 189

ЛАБОРАТОРНА РОБОТА № 8 ................................................................................................................................... 189

ВИКОРИСТАННЯ ТЕХНОЛОГІЇ OLE ..................................................................................................................... 189

ОСНОВНІ ТЕОРЕТИЧНІ ВІДОМОСТІ ................................................................................................................................ 189 ПОРЯДОК ВИКОНАННЯ РОБОТИ ......................................................................................................................... 190 КОНТРОЛЬНІ ЗАПИТАННЯ .................................................................................................................................... 192

РЕКОМЕНДОВАНА ЛІТЕРАТУРА ........................................................................................................................... 193

Page 5: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

5

Вступ. Мета та задачі предмету. Системне програмування. Системне

програмне забезпечення. Етапи створення програм.

Основні розділи теми.

1. Системне програмне забезпечення.

2. Системне програмування.

3. Стиль.

4. Інтерфейс.

5. Відладка.

6. Тестування.

Питання для самоконтролю:

1. На які групи поділено системне програмне забезпечення.

Прокоментуйте кожну групу.

2. Системне програмування, що це таке?

3. Стиль програмування.

4. Інтерфейс.

5. Відладка програм.

6. Тестування.

Література: 12 с. 22-48, 6 с. 109-114

Системне програмне забезпечення. Системне програмування.

Системне програмне забезпечення – розділено на 5 груп:

1. Операційні системи.

2. Системи управління файлами. (Це організація зручнішого доступу до

даних, організованих як файли, завдяки якій замість низькорівневого доступу

до даних з вказівкою конкретних фізичних адрес потрібного нам запису

використовується логічний доступ з вказівкою імені файлу і запису у ньому)

3. Інтерфейсні оболонки – для взаємодії користувача з ОС і програмні

середовища. Операційне середовище визначається програмними

інтерфейсами, тобто API (application program interface). Інтерфейс

прикладного програмування (API) включає управління процесами, пам'яттю і

введенням/виведенням.

4. Системи програмування. – транслятор з відповідної мови, бібліотеки

підпрограм, редактори, компонувальники і відладчики.

5. Утиліти – спеціальні системні програми, за допомогою яких можна як

обслуговувати саму операційну систему, так і готувати для роботи носії

даних, виконувати перекодування даних, здійснювати оптимізацію

розміщення даних на носієві і проводити деякі інші роботи, пов'язані з

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

розбиття накопичувача на магнітних дисках на розділи, програма

форматування, програма перенесення основних системних файлів самою ОС і

комплекси програм від фірми Symantec, що носять ім'я Пітера Нортона

(творця цієї фірми і співавтора популярного набору утиліт для перших IBM

PC). Природно, що утиліти можуть працювати тільки у відповідному

Page 6: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

6

операційному середовищі.

Системне програмування – це не просто створення програм на мові

програмування, але і уміння зробити програму швидше, економічніше або краще в

тому або іншому відношенні. Це уміння правильно використовувати

багатозадачність, управляти виділенням оперативної пам'яті і організовувати

обмін даними між процесами

В світі різноманітних інтерфейсів, постійно змінних мов, систем і утиліт, під

постійним тиском обставин ми часто втрачаємо з вигляду головні принципи, які

повинні бути підставою будь-якої хорошої програми, — простота, чіткість і

універсальність.

Не приділяється належної уваги інструментам і нотаціям, способам запису,

які механізують деякі аспекти створення програм, тобто привертають до процесу

програмування сам комп'ютер.

Написати програму — це більш ніж добитися правильного синтаксису,

виправити помилки і примусити її виконуватись досить швидко. Програми

читаються не тільки комп'ютерами, але і програмістами. А програму, написану

добре, куди простіше зрозуміти, чим написану погано. Культура в написанні коду

допоможе створювати програми, помилок в яких буде значно менше. На щастя,

дотримувати дисципліну не важко.

Стиль

Принципи хорошого стилю програмування полягають зовсім не в наборі

якихось обов'язкових правил, а в здоровому глузді, яке витікає з досвіду. Код

повинен бути простий і зрозумілий: очевидна логіка, природні вирази,

використання угод, прийнятих в мові розробки, осмислені імена, акуратне

форматування, розгорнені коментарі, а також відсутність хитрих трюків і

незвичайних конструкцій. Логічність і зв'язність необхідні, тому що іншим

простіше читати код, написаний вами, а вам, відповідно, — їх код, якщо всі

використовуватимуть один і той же стиль. Деталі можуть бути продиктовані

місцевими угодами, вимогами менеджменту або самою програмою, але навіть якщо

це не так, то краще всього підкорятися найбільш поширеним угодам.

Ім'я змінної або функції позначає об'єкт і містить деяку інформацію про його

призначення. Ім'я повинне бути інформативним, лаконічним, таким, що

запам'ятовується і, по можливості, вимовним. Багато що стає ясним з контексту і

області видимості змінної: чим більше область видимості, тим більше

інформативним повинне бути ім'я.

Використовуйте осмислені імена для глобальних змінних і короткі — для

локальних. Глобальні змінні за визначенням можуть виявитися в будь-якому місці

програми, тому їх імена повинні бути достатньо довгими і інформативними, щоб

нагадати читачеві про їх призначення. Корисно опис кожної глобальної змінної

забезпечувати коротким коментарем:

int npending = 0; // поточна довжина черги введення

Глобальні функції, класи і структури також повинні мати осмислені імена,

що пояснюють їх роль в програмі.

Page 7: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

7

Для локальних змінних, навпаки, краще підходять короткі імена; для

використання усередині функції цілком зійде просто п, непогано буде виглядати

npoints, а ось number().f Points буде явним перебором.

Існує безліч угод і місцевих традицій іменування. До найбільш поширених

правил відносяться використання для вказівників імен, що починаються або

закінчуються на р (від poiter, вказівник), наприклад nodep, а також заголовних букв

на початку імен Глобальних змінних і всіх заголовних — в іменах КОНСТАНТ.

Деякі фірми програмістів дотримуються радикальніших правил, таких як

відображення в імені змінної інформації про її тип і спосіб використання.

Наприклад, ріntch означатиме вказівник на символьний тип (pointer to char), а

strFrom і strTo — рядки для введення і виведення, відповідно. Що ж до власного

написання імен, то використання npending, numPending або num_pending залежить

від особистих пристрастей, і принципової різниці немає. Головне — дотримувати

основні смислові угоди; все, що не виходить за їх рамки, вже не так важливо.

Використання угод полегшить вам розуміння як вашого власного коду, так і

коду, написаного іншими програмістами. Крім того, вам набагато простіше

придумуватиме імена для нових змінних, що з'являються в програмі. Чим більше

розмір вашого коду, тим важливіше використовувати інформативні,

систематизовані імена.

Будьте послідовні. Схожим об'єктам давайте схожі імена, які б показували їх

схожість і підкреслювали відмінності між ними.

Використовуйте активні імена для функцій. Ім'я функції повинне базуватися

на активному дієслові (у дійсній заставі), за яким може слідувати іменник:

now = date.getTime();

putchar('\n');

Будьте точні. Як ми вже вирішили, ім'я не тільки позначає об'єкт, але і надає

читачеві деяку інформацію про нього. Невірно підібране ім'я може стати причиною

абсолютно таємничих помилок.

Отже, імена треба підбирати так, щоб максимально полегшити життя

читачеві; так само і вирази слід писати так, щоб їх сенс був гранично ясний.

Пишіть найясніший код, який тільки можливий. Вставляйте пропуски навколо

операторів для логічного угрупування виразів; взагалі, форматуйте вирази так, щоб

зробити їх найбільш легкими для читання. Ідея тривіальна, але дуже корисна:

зовсім як думка про те, що чим акуратніше прибраний ваш робочий стіл, тим

простіше на нім знайти необхідну річ. Правда, на відміну від робочого столу у

вашій програмі можуть копатися і сторонні — і їм теж повинно бути зручно.

Чим логічніше і послідовніше оформлена програма, тим вона краща. Якщо

форматування коду міняється непередбачуваним чином: цикл проглядає масив то

за збільшенням, то по убуванню, рядки копіюються те за допомогою strcpy, то за

допомогою циклу for — всі перераховані варіації збивають читача з пантелику, і

йому важко зрозуміти, що насправді відбувається. Але якщо одні і ті ж обчислення

завжди виконуються однаково, то кожне відхилення від звичайного стилю

указуватиме на дійсну відмінність, яка заслуговує бути відмічена.

Page 8: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

8

Коментарі повинні допомагати читати програму. Вони не допоможуть,

повторюючи те, що і так зрозуміло з коду або якщо заперечують коду або

відволікаючи читача від суті друкарськими хитруваннями. Кращий коментар

допомагає зрозуміти програму, стисло відзначаючи найбільш значущі деталі або

надаючи укрупнену картину що відбувається.

У коментарях повинні міститися інформація, яка не витікає з коду, або ж

інформація, що відноситься до великого фрагменту коду і зібрана в одне місце.

Коли в коді відбувається щось важко вловиме, коментар повинен уточнювати що

відбувається, але якщо всі дії і так очевидні, описувати їх ще раз в словах просто

безглуздо:

Основна думка полягає в тому, що хороший стиль повинен просто увійти до

звички. Якщо ви замислюватиметеся про стиль при написанні коду, якщо ви

виділятимите час для того, щоб перевіряти і покращувати його стиль, ви виробите

у себе дуже корисну звичку. Після того, як все це ви пророблятимете автоматично,

ваша підсвідомість подбає про багато деталей, і навіть код, який ви писатимете в

поспіху, стане набагато краще.

Проектування структур даних — центральний момент в створенні програми.

Після того, як структури даних визначені, алгоритми, як правило, прагнуть самі

встати на своє місце, і кодування стає відносно простою справою.

Підсумки:

Використовуйте осмислені імена для глобальних змінних і короткі — для

локальних.

Будьте послідовні.

Використовуйте активні імена для функцій.

Будьте точні.

Форматуйте код, підкреслюючи його структуру.

Використовуйте природну форму виразів.

Використовуйте дужки для усунення неясності.

Розбивайте складні вирази.

Будьте простіше.

Будьте обережні з побічними ефектами.

Будьте послідовні в застосуванні відступів і фігурних дужок.

Використовуйте ідіоми для єдності стилю.

Використовуйте else-if для багатоваріантних галужень.

Уникайте макрофункцій.

Беріть тіло макросу і аргументи в дужки.

Давайте імена загадковим числам.

Визначайте числа як константи, а не як макроси.

Використовуйте символьні константи, а не цілі.

Використовуйте засоби мови для визначення розміру об'єкту.

Не пишіть про очевидне.

Коментуйте функції і глобальні дані.

Не коментуйте поганий код, а перепишіть його.

Не суперечте коду.

Вносьте ясність.

Page 9: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

9

Інтерфейс

Інтерфейс -- це деталізована, описана межа взаємодії між кодом, що надає

деякі можливості, і кодом, який ці можливості використовує. Інтерфейс визначає,

що саме надає своєму користувачеві деякий закінчений блок коду, яким чином

функції (а може бути, і якісь елементи даних) з цього блоку можуть бути

використані в решті частини програми. Інтерфейс CSV надає користувачеві три

функції: читання рядка, отримання поля і повернення кількості полів. Окрім них,

користувач не може одержати від нашого коду нічого.

Для того, щоб виявитися зручним, інтерфейс повинен відповідати деяким

базовим вимогам: бути простим, загальним, стандартним, передбаченим, надійним,

а також нести в собі можливість без втрат адаптуватися до змін запитів

користувачів і своєї внутрішньої реалізації. У основі хороших інтерфейсів лежать

декілька принципів. Принципи ці тісно взаємозв'язані, а іноді навіть суперечливі,

але вони допоможуть нам описати, що ж відбувається при перетині кордону між

двома частинами програми.

Відладка

Одна з причин складності програми полягає у великій кількості способів, за

допомогою яких можуть взаємодіяти її компоненти, а вже програми повні із

компонентами, і взаємозв'язками між ними. Багато технологій намагаються

скоротити зв'язки між компонентами, щоб зменшити кількість взаємодій:

наприклад, використовується заховання інформації, абстрагування і інтерфейси, а

також всі можливості мов, сприяючі цим технологіям. Існують також технології

для перевірки цілісності архітектури програми: докази коректності програм,

моделювання, аналіз вимог, формальні перевірки. Жодна з перерахованих

технологій не змінила радикально способу створення програм: вони працюють

лише на невеликих завданнях. У реальності завжди будуть помилки, які ми

знаходимо за допомогою тестування і усуваємо за допомогою відладки (debugging).

Хороші програмісти знають, що вони проведуть стільки ж часу, відладжуючи

програму, скільки вони її і писали, і тому прагнуть вчитися на своїх помилках.

Кожна знайдена помилка зможе навчити вас, як запобігти появі подібної помилки в

майбутньому і як справитися з нею, якщо вона все ж таки з'явиться.

Відладка складна і може займати непередбачуване довгий час, тому мета в

тому, щоб минути велику її частину. Технічні прийоми, які допоможуть зменшити

час відладки, включають хороший дизайн, хороший стиль, перевірку граничних

умов, перевірку правильності (результатних) тверджень і розумності коду, захисне

програмування, добре розроблені інтерфейси, обмежене використання глобальних

даних, засоби контролю і перевірки. Грам профілактики коштує тонни лікування.

Компілятори основних мов програмування зазвичай поставляються із

складними відладчиками, що часто входять до складу середовища програмування,

яке об'єднує в собі створення і редагування початкового коду, компіляцію,

виконання і відладку. Відладчики включають графічний інтерфейс для покрокового

виконання програми, оператор за оператором або функція за функцією, із

зупинками на конкретних рядках програми або досягши якоїсь умови. Вони також

надають можливість форматування і відображення значень змінних.

Page 10: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

10

Відладчик можна використовувати безпосередньо, якщо існуюча проблема

точно відома. Деякі відладчики включаються автоматично, якщо під час виконання

програми щось відбувається не так, як слідує. Зазвичай досить легко виявити, в

якому місці виконувалася програма, якщо вона несподівано аварійно завершилася,

при цьому можна розглянути послідовність функцій, що виконувалися в той

момент (це називається "Переглядання стека викликів"), а також відобразити

значення локальних і глобальних змінних. Цієї інформації буває достатньо, щоб

виявити помилку. Інакше можна повторно запустити програму в покроковому

режимі, щоб виявити, де саме починається невірна поведінка.

Процес відладки включає зворотне трасування (backward reasoning) —

дослідження подій в зворотному порядку, як в детективі. Трапилося щось

неможливе, і єдине, що відомо точно, — неможливе трапилось. Для того, щоб

розкрити причини, потрібно в думках проходити зворотній шлях від результату до

можливої причини. Коли у нас є повне пояснення, ми знаємо, що саме виправляти

і, по ходу діла, швидше за все, виявимо декілька інших речей, яких ми не чекали.

Не відкладайте відладку на потім. Надмірна квапливість може пошкодити і в

інших ситуаціях. Не ігноруйте помилку, що виявилася: відстежте її прямо зараз,

тому що потім вона може і не виникнути. Приклад — знаменита історія, що

трапилася при запуску космічної станції "Mars Pathfinder". Після бездоганного

"приземлення" в липні 1997 року комп'ютери станції мали звичай

перезавантажуватися в середньому один раз в день, і це поставило інженерів в

безвихідь. Коли вони відстежили помилку, то зрозуміли, що вже зустрічалися з

нею. Під час передпускових перевірок такі перезавантаження траплялися, але були

проігноровані, тому що інженери працювали над іншими питаннями. Тепер вони

вимушені вирішувати проблему, коли машина знаходиться на відстані десятків

мільйонів кілометрів, і виправити помилку стало значно важче.

Тестування

«У практиці обчислень в ручну або за допомогою настільної машини, треба,

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

локалізувати її, повторивши процес в зворотному порядку з тієї точки, де помилка

була виявлена вперше.» Норберт Вінер. Кібернетика

Тестування і відладка часто згадуються разом, проте це дві різні речі. Сильно

спрощуючи, можна сказати, що відладкою називається те, що ви робите, коли

знаєте, що програма не працює. Тестування ж — це послідовні, систематичні

спроби добитися помилки від програми, яка вважається такою, що працює.

Эдсгеру Дейкстре (Edsger Dijkstra) належить відомий вислів про те, що

тестування може показати лише наявність помилок, але не їх відсутність. Він

сподівається на те, що творці програм зможуть писати їх коректно, тобто без

помилок взагалі, і, отже, в тестуванні не буде ніякої необхідності. Це, звичайно,

відмінна мета, і до її досягнення варто прагнути, але для справжніх (комерційних)

програм це поки нереально.

Тестуйте граничні умови коду. Одним з найважливіших методів тестування є

тестування граничних умов: кожного разу, написавши невеликий шматок коду,

наприклад цикл або умовний вираз, перевірте, що тіло циклу повториться потрібну

кількість разів, а умовний вираз правильно розгалужує обчислення. Цей процес

Page 11: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

11

називається тестуванням граничних умов тому, що ви перевіряєте крайні,

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

введений елемент, повністю заповнений масив і т.п. Основна ідея полягає в тому,

що більшість помилок виникають якраз на межах — при якихось екстремальних

значеннях. Якщо якийсь блок коду містить помилку, то, швидше за все, ця помилка

відбувається на межі, і навпаки — якщо при екстремальних значеннях код працює

коректно, то він практично напевно працюватиме коректно і всюди.

Тестуйте перед і пост умови. Ще один спосіб запобігти виникненню проблем

— упевнитися в тому, що очікувані або необхідні умови задовольняються до

(передумова) або після (пост умова) виконання деякого блоку коду.

Дуже важливо тестувати програму систематично — на кожному етапі треба

чітко уявляти, що ви тестуєте в даний момент і яких результатів чекаєте.

Тестування повинне проводитися послідовно, щоб нічого не упустити: поточні

результати тестування треба обов'язково записувати, щоб уявляти, що вже

зроблено і що належить зробити.

Тестуйте за зростанням. Тестування повинне йти рука в руку із створенням

коду. Тестування методом "великого стрибка", коли спочатку пишеться вся

програма, а потім тестується цілком, набагато складніше і віднімає значно більше

часу, чим поступове. Напишіть частину програми, відтестуйте її, напишіть

черговий шматок коду, відтестуйте його і т.д. Якщо у вас є два блоки, які писалися

і тестувалися роздільно, відтестуйте їх взаємодію.

Підсумки:

Тестуйте граничні умови коду.

Тестуйте перед- і пост умови.

Використовуйте твердження.

Використовуйте підхід захисного програмування.

Перевіряйте коди повернення функцій.

Тестуйте по тій, що зростає.

Тестуйте спочатку прості блоки.

Чітко визначте, чого ви чекаєте на виході тесту.

Перевіряйте властивості збереження даних.

Порівняйте незалежні версії.

Оцінюйте обхват тестів.

Автоматизуйте зворотне тестування.

Створюйте замкнуті тести.

Змістовний модуль 1. Вказівники та посилання. Динамічний розподіл пам'яті. Динамічні масиви.

Основні розділи теми.

1. Вказівники.

2. Динамічні масиви.

3. Динамічні двовимірні масиви.

4. Вказівки на функції.

Page 12: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

12

5. Вказівники - аргументи функцій.

6. Масив вказівників на функції.

7. Рядки та літерали.

8. Робота з даними рядкового типу.

Питання для самоконтролю.

1. Який оператор повертає адресу змінної?

2. Який оператор використовується для набуття значення, що зберігається

за адресою, що знаходиться в вказівнику?

3. Що таке вказівник?

4. У чому різниця між адресою, що зберігається в вказівнику, і значенням,

записаним за цією адресою?

5. Чим відрізняється оператор непрямого доступу від оператора звернення

до адреси?

6. У чому різниця між оголошеннями

const int * ptrOne і

int * const ptrTwo?

7. Що тут оголошене?

int * pOne;

int vTwo;

int * pThree = &vTwo;

8. Якщо оголошена змінна типу unsigned short на ім'я yourAge, то як оголосити

вказівник на неї?

9. Присвойте змінною yourAge значення 50 за допомогою вказівника,

оголошеного у вправі 7.

10. Напишіть невелику програму і оголосіть в ній змінну типу int і вказівник

на неї. Присвойте вказівнику адресу змінної. Використовуючи вказівник,

присвойте змінной яке-небудь значення.

11. Відладка: Знайдіть помилку в наступному фрагменті коду.

#include <iostream>

using namespace std;

int main()

{

int *pInt;

*pInt = 9;

cout « "The value at pint: " « *pInt;

return 0;

}

12. Відладка: Знайдіть помилку в наступному фрагменті коду.

int main()

{

int SomeVariable = 5;

cout « "SomeVariable: " « SomeVariable « "\n";

int *pVar = & SomeVariable;

pVar = 9;

Page 13: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

13

cout « "SomeVariable: " « «pVar « "\n"; return 0;}

13. Які операції дозволені над вказівниками?

14. Яким чином необхідно працювати з масивом, якщо до початку роботи

невідомо, скільки в масиві елементів

15. int n=10;

int a*=new int [n] - що описане а цьому рядку?

16. Чи можна ініціалізувати динамічний масив?

17. При виділенні пам'яті під динамічний масив чи відбувається обнулення

пам'яті?

18. Як звернутися двома способами до третього елементу масиву?

19. Що треба зробити в програмі, якщо в якийсь момент роботи програми

динамічний масив перестає бути потрібним?

20. Що таке витік пам'яті?

21. Вказівники - аргументи функцій.

22. Яким чином вказівник на функцію визначається?

23. Як провести виклик функції з передачею параметрів по посиланню?

24. Посилальні параметри.

25. Вказівники на функції.

26. Виклик функції за допомогою вказівника на функцію.

27. Масиви вказівників на функції.

Література: 9 с. 51-58, с. 286-.294. 7 с. 68-72, с. 210-274,.

Вказівки та посилання. Динамічні масиви.

Вказівник - це змінна, що містить адресу пам'яті, де розташовані інші

об'єкти (змінні, функції і т.п.)

Вказівник це група комірок (як правило дві або чотири), в яких може зберігатися

адреса. Так, якщо с має тип char, а р – вказівник, на с, то ситуація виглядає таким

чином:

У мові С++ широко використовується поняття адреси змінних. Робота з

адресами дісталася С++ як спадок від мови С.

Припустимо, що в програмі:

int х; // визначена змінна типу int

int* xptr; // Можна визначити змінну типу вказівник на ціле число

xptr = &х; // присвоїти змінній xptr адресу змінної х

Операція &, застосована до змінної - це операція узяття адреси. Операція *,

застосована до адреси - це операція звернення за адресою.

Таким чином, два оператори еквівалентні:

int у = х; // привласнити змінній у значення х

Page 14: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

14

int у = *хрtr; // привласнити змінній у значення

// що знаходиться за адресою хрtr

За допомогою операції звернення за адресою можна записувати значення:

*xptr = 10; // записати число 10 за адресою xptr

Після виконання цього оператора, значення змінної х стане рівним 10,

оскільки xptr указує на змінну х.

Вказівник - це не просто адреса, а адреса величини визначеного типу.

Вказівник xptr - адреса цілої величини. Визначити адреси величин інших типів

можна таким чином:

unsigned long* IPtr; // вказівник на ціле число без знаку

char* cp; // вказівник на байт

Complex* p; // вказівник на об'єкт класу Complex

Наступні декілька рядків показують, яким чином оголошуються вказівники і

використовуються оператори & і *.

int x=1, y=22, z[10];

int *ip; // ip – вказівник на int

ip=&x; // тепер ip вказує на х

y=*ip; // у тепер рівний 1

*ip=0 // x тепер рівний 0

ip=&z[0]; //ip тепер указує на z[0]

Для чого потрібні вказівники?

1. Вказівники з'явилися, перш за все, для потреб системного програмування.

Оскільки мова С призначалася для "низкорівнего" програмування, на нім

потрібно було звертатися до, наприклад, регістрам пристроїв. У цих регістрів

цілком певні адреси, тобто необхідно було прочитати або записати значення за

певною адресою. За допомогою механізму вказівників такі операції не

вимагають ніяких додаткових засобів мови.

int* hardwareRegister = 0x80000;

*hardwareRegister = 12;

2. Проте використання вказівників далеко не обмежується потребами системного

програмування. Вказівники дозволяють спростити і прискорити ряд операцій.

Припустимо, в програмі є область пам'яті для зберігання проміжних результатів

обчислень. Цю область пам'яті використовують різні модулі програми. Замість

того, щоб кожного разу при зверненні до модуля копіювати цю область пам'яті,

ми можемо передавати вказівник. як аргумент виклику функції, тим самим

спрощуючи і прискорюючи обчислення.

Адресна арифметика

З вказівниками можна виконувати не тільки операції присвоєння і

звернення за адресою, але і ряд арифметичних операцій.

1. Вказівники одного і того ж типа можна порівнювати за допомогою

стандартних операцій порівняння. При порівнянні вказівників порівнюються їх

значення, а не значення величин, на які дані вказівники указують. Так в

Page 15: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

15

нижчеприведеному прикладі результат першої операції порівняння буде

помилковим:

int x = 10; int у = 10;

int* xptr = &х; int* yptr = &y;

// порівнюємо вказівники

if (xptr == yptr) {

cout « "Вказівники рівні" « endl;

} else {

cout « "Вказівники нерівні" « endl;

}

//порівнюємо значення, на які указують вказівники

if (*xptr = = *yptr) {

cout « "Значення рівні" « endl;

} else {

cout « " Значення нерівні" « endl;

}

Проте результат другої операції порівняння буде істинним, оскільки змінні х

і у мають одне і те ж значення.

2. Крім того, над вказівниками можна виконувати обмежений набір арифметичних

операцій.

2.1. До вказівника можна додати або відняти ціле число. Результатом

збільшення одиниці до вказівника є адреса наступної величини типу, на

який указує вказівник, в пам'яті. Пояснимо це на малюнку. Хай xPtr -

вказівник на ціле число типу long, а cp - вказівник на тип char.

Починаючи з адреси 1000 в пам'яті розташовано два цілі числа. Адреса

другого - 1004 (у більшості реалізацій С++ під тип long виділяється

чотири байти). Починаючи з адреси 2000 розташовані об'єкти типу char.

Мал. 5. Адресна арифметика.

Розмір пам'яті, що виділяється для числа типу long і для char різний. Тому

зміна адреси при збільшенні xPtr і ср теж різна. Проте і в тому і в іншому випадку

збільшення вказівника на одиницю означає перехід до наступної в пам'яті

величини того ж типу. Збільшення або віднімання будь-якого цілого числа працює

Адрес 1000 Адрес 1004

xPtr xPtr+1

2000 2001 2002

cp + 2 cp + 1

cp

Page 16: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

16

за тим же принципом, що і збільшення на одиницю. Вказівник зрушується вперед

(при збільшенні додатного числа) або назад (при відніманні додатного числа) на

відповідну кількість об'єктів того типу, на який вказівник указує. Взагалі кажучи

неважливо, об'єкти якого типу насправді знаходяться в пам'яті - адреса просто

збільшується або зменшується на необхідну величину. Насправді значення

вказівника i ptr завжди змінюється на число, рівне sizeof (*ptr).

a. Вказівники одного і того ж типу можна віднімати один з одного.

Різниця вказівників показує, скільки об'єктів відповідного типу

може поміститися між вказаними адресами.

Унарні оператори * і & мають вищий пріоритет, ніж арифметичні

оператори, так що зустрівши присвоєння y=*ip+1 компілятор візьме те, на що

указує iр і додасть до нього 1, а результат присвоїть змінній у. Аналогічно *ip+=1

збільшує на одиницю те, на що посилається ip; ті ж дії виконують ++*ip і (*ip)++.

У останньому записі дужки необхідні, оскільки, якщо їх не буде, збільшиться

значення самого вказівника, а не те, на що він посилається. Це обумовлено тим,

що унарні оператори * і ++ мають однаковий пріоритет і порядок виконання -

справа наліво. І, нарешті, оскільки вказівники самі є змінними, в тексті вони

можуть зустрічатися і без оператора розкриття посилання. Наприклад, якщо iq є

вказівник на int, то iq=ip копіює вміст ip в iq, щоб ip і iq посилалися на один і той

же об'єкт. Вказівники можна використовувати як операнди в арифметичних

операціях. Якщо у - вказівник, то унарна операція у++; збільшує його значення;

тепер воно є адресою наступного елементу.

Будь-яку адресу можна перевірити на рівність (==) або нерівність (!=) із

спеціальним значенням NULL, яке дозволяє визначити нічого не адресуючий

вказівник.

Вказівники і Масиви.

У С++ існує тісний зв'язок між вказівниками і масивами. Будь-який доступ

до елементу масиву, здійснюваний операцією індексування, може бути виконаний

за допомогою вказівника.

Оголошення int а[10]; визначає масив а розміру 10, тобто блок з 10

послідовних об'єктів з іменами а[0], а[1] ..., а[9].

Запис а[i] посилає нас до i-го елементу масиву. Якщо pa є вказівник на int,

тобто визначений як int *pa; то в результаті присвоєння pa=&a[0]; pa указуватиме

на нульовий елемент а; інакше кажучи, ра міститиме адресу елементу а[0].

Page 17: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

17

Тепер присвоєння

x=*pa; копіюватиме вміст а[0] в х.

Якщо ра указує на деякий елемент масиву,

то ра+1 за визначенням вказує на наступний елемент,

ра+i на i-ий елемент після ра, а

ра-i на i-ий елемент перед ра.

Таким чином, якщо

ра вказує на а[0], то *(ра+1) є вміст а[1],

pa+i - адреса а[i], а *(pa+i) - вміст а[i].

Між індексуванням і арифметикою з вказівниками існує дуже тісний зв'язок.

За визначенням ім'я масива - це адреса його нульового елементу. Після

присвоєння pa=&a[0]; pa і а мають одне і те ж значення. Оскільки ім'я масиву є не

що інше, як адреса його початкового елементу, присвоєння ра=&a[0]; можна

також записати в наступному вигляді: ра=a;

Ще дивовижніше (принаймні на перший погляд) те, що а[i] можна записати

як *(a+i).

Зустрічаючи запис а[i], компілятор відразу перетворить його в * (а+i);

вказані дві форми запису еквівалентні. З цього виходить, що одержані в

результаті застосування, оператора & записи &a[i] і a+i також будуть еквівалентні,

тобто і в тому і в іншому випадку це адреса i-го елементу після а. З іншого боку,

якщо ра - вказівник, то у виразах його можна використовувати з індексом, тобто

запис ра[i] еквівалентний запису *(ра+i). Елемент масиву однаково дозволяється

зображати і у вигляді вказівника із зсувом, і у вигляді імені масиву з індексом.

Зв'язок між масивами і вказівниками

Існує певний зв'язок між вказівниками і масивами. Припустимо, є масив з

100 цілих чисел. Запишемо двома способами програму підсумовування елементів

цього масиву:

long array[100];

long sum = 0;

for (int i = 0; i < 100; i++)

sum += array[i];

long array[100];

long sum = 0;

for (long* ptr = &array[0];

ptr < &array[99] + 1; ptr++)

sum += *ptr;

Page 18: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

18

Елементи масиву розташовані послідовно в пам'яті і збільшення вказівника

на одиницю означає зсув до наступного елементу масиву. Згадка імені масиву без

індексів перетвориться до адреси його першого елементу:

for (long* ptr = array; ptr < &array[99] + 1; ptr++)

sum += *ptr;

Хоча змішення вказівників і масивів цілком законно, я б не стала

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

При використанні багатовимірних масивів вказівники дозволяють

звертатися до зрізів або підмасивів. Якщо ми оголосимо тривимірний масив exmpl:

long exmpl[5][6][7]

то вираз виду exmpl [1] [1] [2] - це ціле число, exmpl [1] [1] - вектор цілих чисел

(адреса першого елементу вектора, тобто має тип long), exmpl [l] - двовимірна

матриця або вказівник на вектор (тип (* long) [7]). Таким чином, задаючи не всі

індекси масиву, ми одержуємо вказівники на масиви меншої розмірності.

Оператори вільної пам'яті new і delete

Ви вже маєте досвід написання програм і, напевно, потрапляли за ситуації,

коли необхідно було вирішити питання подібні наступним:

1. Як динамічно виділяти пам'ять під масив? Тобто, щоб користувач задавав

сам розмірність масиву або програміст міг в потрібний момент змінити

розмірність масиву.

2. Як динамічно створювати і знищувати змінні? Тобто, щоб програміст міг

безпосередньо сам створювати і видаляти змінні.

Унарні оператори new і delete служать для управління вільною пам'яттю.

Що таке вільна пам'ять?

Вільна пам'ять - це область пам'яті, що надається системою, для об'єктів, час

життя яких безпосередньо управляється програмістом.

Програміст створює об'єкт за допомогою ключового слова new

(перекладається з англійського як ''новий''), а знищує його, використовуючи delete

(перекладається з англійського як ''видалити'').

У С++ оператор new приймає наступні форми:

new і'мя_типу;

new і'мя_типу (ініціалізатор);

new і'мя_типу [вираз];

У кожному випадку відбувається принаймні два ефекти. По-перше,

виділяється належний об'єм вільної пам'яті для зберігання вказаного типу. По-

друге, повертається базова адреса об'єкту (як значення оператора new ). Коли

пам'ять недоступна, оператор new повертає значення 0 (NULL). Отже, ми можемо

контролювати процес успішного виділення пам'яті оператором new. Розглянемо

наступний приклад використання оператора new:

int *p, *q;

Page 19: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

19

p=new int (5); //виділили пам'ять і ініціалізували

q=new int [10]; //отримуємо масив від q[0] до q[9]

У цьому фрагменті вказівнику на ціле р присвоюється адреса елементу

пам'яті, одержана при розміщенні цілого об'єкту. Місце в пам'яті, на яке указує р,

ініціалізувалося значенням 5. Такий спосіб зазвичай не використовується для

простих типів ніби int, оскільки набагато зручніше і природніше оголосити змінну

звичним для нас чином.

Оператор delete знищує об'єкт, створений за допомогою new, віддаючи тим

самим розподілену пам'ять для повторного використання. Оператор delete може

приймати наступні форми:

delete вираз;

delete [] вираз;

Перша форма використовується, якщо відповідний вираз new розміщував не

масив. У другій формі присутні порожні квадратні дужки, що показують, що

спочатку розміщувався масив об'єктів. Оператор delete не повертає значення, тому

можна сказати, що повертаємий ним тип - void.

Динамічні двовимірні масиви.

При рішенні на комп'ютері серйозних завдань, наприклад, при розробці

додатків, що інтенсивно використовують ресурси графіки, завжди потрібно мати

під рукою достатню кількість ресурсів GDI (Graphics Device Interface), які

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

пам'яттю часто набувають вирішального значення.

Більш універсальний і безпечний спосіб виділення пам'яті під двовимірний

масив, коли обидві його розмірності задаються на етапі виконання програми,

приведений нижче:

int nrow, ncol;

cout « " Введіть кількість рядків і стовпців :": cin » nrow » ncol:

int **a = new int *[nrow]; // 1

for(int i = 0: i < nrow; i++) // 2

а[i]= new int [ncol]: // 3

У операторі 1 оголошується змінна типу «вказівник на вказівник на int» і

виділяється пам'ять під масив вказівників на рядки масиву (кількість рядків —

nrow). У операторі 2 організовується цикл для виділення пам'яті під кожен рядок

масиву. У операторі 3 кожному елементу масиву вказівників на рядки

присвоюється адреса початку ділянки пам'яті, виділеного під рядок двовимірного

масиву. Кожен рядок складається з ncol елементів типу int.

Page 20: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

20

Вказівки та посилання. Вказівки на функції.

Вказівки на масив, як параметри. Масив вказівників на функції.

Вказівники - аргументи функцій.

Для того, щоб розібратися з механізмом передачі аргументів у функцію, з

подальшою зміною їх значень усередині викликаної функції, розглянемо функцію

my_swap, яка міняє місцями значення своїх параметрів. Іншими словами, якщо

оголошені дві змінні типу int а, b; причому, a=7 і b=10, то після виклику функції

my_swap(&а &b) результат буде наступним: а=10 і b=7.

У самій функції my_swap параметри повинні бути описані як вказівники, при

цьому доступ до значень параметрів здійснюватиметься через них побічно. Такий

виклик функції називається викликом функції з передачею параметрів по

посиланню, причому, при зверненні до функції адреси змінних повинні

передаватися як аргументи. Розглянемо реалізацію функції, яка змінюватиме

значення своїх параметрів:

void my_swap(int *px, int *py)

{

int temp;

//проводимо стандартну перестановку через додаткову змінну temp

temp=*px; //змінной temp присвоїли 7

*px=*py; // а прийняла значення 10

*py=temp; // змінна b прийняла значення 7

}

#include <iostream.h>

void main()

{

int а,b;

cout<<"Enter а = ";

Page 21: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

21

cin>>a;

cout<<"Enter b = ";

cin>>b;

my_swap(&а &b); //ВИКЛИК ФУНКЦІЇ З ПЕРЕДАЧЕЮ ПАРАМЕТРІВ ЗА ПОСИЛАННЯМ

cout<<"New value of а = "<<a<<endl;

cout<<"New value of b = "<<b<<endl;

}

Підсумки:

аргументи-вказівники дозволяють функції здійснювати доступ до об'єктів

функції, що викликала її, і дають їй можливість змінити ці об'єкти. Якщо Ви

хочете провести виклик функції з передачею параметрів за посиланням з

використанням вказівників, дотримуйтеся наступних інструкцій:

1. Оголосіть параметри-вказівники в заголовку функції.

2. Використовуйте «розіменованний» вказівник в тілі функції.

3. Передавайте адреси як аргументи за викликом функції.

Посилальні параметри

У разі передачі параметрів за посиланням функція, що викликається, дістає

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

можливість зміни цих даних. Виклик за посиланням має перевагу в сенсі

продуктивності перед викликом по значенню, оскільки він виключає накладні

витрати на копіювання великих об'ємів даних, проте слід пам'ятати що при

виклику за посиланням функція, що викликається, може змінити передані в неї

дані, а це може бути небажано.

Отже, посилальний параметр - це псевдонім відповідного аргументу

(параметра). Щоб показати, що параметр функції переданий за посиланням, після

типу параметра в прототипі функції ставиться символ амперсанда (&, таке ж

позначення використовується в списку типів параметрів в заголовку функції.

Наприклад, оголошення

int &count

у заголовку функції може читатися як "count є посиланням на int". У виклику

функції досить вказати ім'я змінної і вона буде передана за посиланням. Тоді

згадка в тілі функції змінної, що викликається, на ім'я її параметра насправді є

зверненням до початкової змінної в у функції і ця початкова змінна може бути

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

Вказівники на функції

Кожна функція характеризується типом значення, яке повертається, ім'ям,

кількістю, порядком проходження і типами параметрів.

При використанні імені функції без подальших дужок і параметрів ім'я

функції виступає як вказівник на цю функцію, і його значенням служить

Page 22: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

22

адреса розміщення функції в пам'яті. Це значення адреси може бути присвоєне

іншому вказівнику, і потім вже цей новий вказівник можна застосовувати для

виклику функції. Проте у визначенні нового вказівника повинен бути той же тип,

що і значення, яке повертає функція, то ж кількість, порядок проходження і типи

параметрів.

Вказівник на функцію визначається таким чином:

тип функції (*ім’я_вказівника)(специфікація_параметрів);

Наприклад: int(*func1ptr) (char);

визначення вказівника func1ptr на функцію з параметром

типу char, що повертає значення типу int.

Якщо приведену синтаксичну конструкцію записати без перших круглих

дужок, тобто у вигляді

int *fun(char);

то компілятор сприйме її як прототип якоїсь функції з ім'ям fun і

параметром типу char, що повертає значення вказівника типу

int *.

Другий приклад:

char * (*func2Ptr) (char *, int); -

визначення вказівника func2Ptr на функцію з параметрами

типу вказівник на char і типі int, що повертає значення типу

вказівник на char.

У визначенні вказівника на функцію тип значення, яке повертається і сигнатура

(типи, кількість і послідовність параметрів) повинні співпадати з відповідними

типами і сигнатурами тих функцій, адреси яких передбачається присвоювати

вказівнику, що вводиться, при ініціалізації або за допомогою оператора

присвоєння.

/*Приклад визначення і використання вказівників на функції */

#include<iostream.h>

void f1(void)

{

cout << "\n Виконується f1()";

}

void f2(void)

{

cout << "\n Виконується f2()";

}

void main()

{

void (*ptr)(void); // ptr - вказівник на функцію

ptr = f2; // ptr ініціалізувався адресою f2()

(*ptr)(); // виклик f2() за її адресою

ptr = f1; // присвоюється адреса f1()

(*ptr)(); // виклик f1() за її адресою

Page 23: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

23

ptr(); // виклик еквівалентний (*ptr)();

}

Результат виконання програми:

Виконується f2()

Виконується f1()

Виконується f1()

У програмі описаний вказівник ptr на функцію, і йому послідовно присвоюються

адреси функції f2 і fl.

Виклик функції за допомогою вказівника на функцію:

(*і’мя_вказівника)(список_фактичних_параметрів);

Значенням імені_вказівника служить адреса функції, а за допомогою операції

«разыменования» * забезпечується звернення за адресою до цієї функції. Проте

буде помилкою записати виклик функції без дужок у вигляді *ptr(); Річ у тому, що

операція () має вищий пріоритет, ніж операція звернення за адресою *. Отже,

відповідно до синтаксису буде спочатку зроблена спроба звернутися до функції

ptr(). І вже до результату буде віднесена операція «разыменования», що буде

сприйняте як синтаксична помилка.

При визначенні вказівник на функцію може ініціалізуватись. Як значення, що

ініціалізувало, повинна використовуватися адреса функції, тип і сигнатура(типи,

кількість і послідовність параметрів) якої відповідають визначуваному вказівнику.

При присвоєнні вказівників на функції також необхідно дотримувати

відповідність типів повертаних значенні функції і сигнатур для вказівників правої

і лівої частин оператора присвоєння. То ж справедливо і при подальшому виклику

функцій за допомогою вказівників, тобто типи і кількість фактичних параметрів,

використовуваних при зверненні до функції за адресою, повинні відповідати

формальним параметрам функції, що викликається.

/*Виклик функцій за адресами через вказівник */

#include<iostream.h>

int add (int n, int m)

{ return n + m;}

int div (int n, int m)

{ return n % m;}

int mult (int n, int m)

{ return n * m;}

int subt (int n, int m)

{ return n - m;}

void main()

{

int (*par)(int, int); // вказівник на функцію

int а = 6, b = 2; // задаються початкові значення для змінних а і b

char с = '+';

while(с != ' ')

{

Page 24: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

24

cout << "\n Аргументи: а = " << а << ", b = " << b;

cout << "\n Результат для с = \'" << с << "\'" << " рівний ";

switch(c)

{

case '+':

par = add; // par одержує адресу функції add

с = '%'; break;

case '-':

par = subt; // par одержує адресу функції subt

с = ' '; break;

case '*':

par = mult; // par одержує адресу функції mult

с = '-'; break;

case '%':

par = div; // par одержує адресу функції div

с = '*'; break;

}

cout << (*par)(а,b);

/* виклик функції за адресою результат, повернений функцією, виводиться на

екран */

}

cout << endl;

}

Результат виконання програми

Аргументи: а = 6, b = 2. Результат для с = '+' рівний 8

Аргументи: а = 6, b = 2. Результат для с = '%' рівний 0

Аргументи: а = 6, b = 2. Результат для с = '*' рівний 12

Аргументи: а = 6, b = 2. Результат для з = '-' рівний 4

Масиви вказівників на функції.

Вказівники на функції можуть бути об'єднані в масиви.

Наприклад, float (*ptrArray) (char) [4] ; - опис масиву з ім'ям ptrArray з чотирьох

вказівників на функції, кожна з яких має

параметр типу char і повертає значення

типу float.

Щоб звернутися, наприклад, до третьої з цих функцій, буде потрібний такий

оператор: float а = (*ptrArray[2]) ('f');

Змістовий модуль 2. Рядки. Динамічні рядки. Файли.

Питання для самоконтролю.

1. Символьний масив.

2. Символьна константа.

3. Літерал.

4. Вивести на екран рядок і його довжину.

Page 25: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

25

5. Метод getline.

6. Функції gets, puts. Функції сімейства printf, scanf.

7. Операції з рядками

8. Функції strlen, strcpy, strncpy.

9. Функція виділення з рядка лексем.

10. Функція пошуку в рядку символу і підрядка.

11. Функції для порівняння рядків і підрядків, об'єднання рядків.

12. Функції роботи з символами.

13. Функція getchar().

14. Функції, перевіряючі приналежність символу якій-небудь множині.

15. Динамічні рядки.

16. Зберігання інформації за допомогою файлів.

Питання для самоперевірки.

1. Автоматичне виділення пам’яті.

2. Cтатичне виділення пам’яті.

3. Динамічние виділення памяті

4. Характерні помилки при використанні динамічної пам’яті.

5. Використання описувача const.

6. Вказівник на незмінний рядок.

7. Незмінний вказівник.

8. Читання цілого рядка файлового введення

9. Визначення кінця файлу.

10. Перевірка помилок при виконанні файлових операцій.

11. Закриття файлу.

12. Управління відкриттям файлу.

13. Режими відкриття файлу.

14. Виконання операцій читання і запису.

Література: 9 с. 67-72, с. 282-.284, с. 238 4 с. 127-158, с. 268-278.

Рядки та літерали. Робота з даними рядкового типу.

Для того, щоб працювати з текстом, в мові С++ не існує особливого

вбудованого типу даних.

Текст представляється у вигляді послідовності знаків (байтів), що

закінчуються нульовим байтом \0.

Іноді таке уявлення називають Сі-рядки, оскільки воно з'явилося в мові С.

Крім того, в С++ можна створити класи для зручнішої роботи з текстами (готові

класи для представлення рядків є в стандартній бібліотеці шаблонів, з якою ми

познайомимося пізніше).

Це необхідно знати:

1. Рядок можна визначити як масив символів або як вказівник на символ.

2. Будь-який рядок закінчується нульовим символом. (Завдяки цій властивості

Ви завжди можете визначити кінець рядка, якщо у Вас рядок займає меншу

Page 26: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

26

кількість символів, чим та кількість, яка була вказана в квадратних дужках

при оголошенні масиву).

3. Для рядків можлива спрощена початкова ініціалізація (порівняно з не

символьними масивами).

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

оператор введення cin>> І’мя_масиву, і аналогічним чином вивести відразу

весь рядок на екран, використовуючи оператор виведення

cout<< І’мя_масиву.

Для запису строкових констант в програмі використовуються літерали.

Літерал - послідовність знаків, ув’язнена в подвійні лапки:

"Це рядок"

"0123456789"

"*"

1. Символ, ув'язнений в подвійні лапки, відрізняється від символу, ув'язненого

в апострофи:

Літерал "*" позначає два байти: перший байт містить символ зірочки, другий

байт містить нуль.

Константа '*' позначає один байт, що містить знак зірочки.

2. За допомогою літералів можна ініціалізувати масиви:

char alldigits[ ]= "0123456789";

Розмір масиву явно не заданий, він визначається виходячи з розміру

літерала, який його ініціалізує, в даному випадку 11 (10 символів плюс

нульовий байт).

Введення-виведення рядків Для введення-виведення рядків використовуються вже відомі нам об'єкти

cin і cout, і функції, успадковані з бібліотеки С. Розглянемо спочатку перший

спосіб:

#include <iostream.h>

void main()

{

const int n = 80; char s[n];

cin » s;

cout « s « endl;

}

Як бачите, рядок вводиться точно так, як і змінні відомих нам типів.

Запустіть програму і введіть рядок, що складається з одного слова. Запустіть

програму повторно і введіть рядок з декількох слів. У другому випадку

виводиться тільки перше слово. Це пов'язано з тим, що введення виконується до

першого пробільного символу (тобто пропуску, знаку табуляції або символу

перекладу рядка '\n').

Якщо потрібно ввести рядок, що складається з декількох слів, в одну

строкову змінну, використовуються методи getline або get класу istream,

об'єктом якого є cin.

Page 27: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

27

Єдине, що нам поки потрібно знати, це синтаксис виклику методу — після

імені об'єкту ставиться крапка, а потім пишеться ім'я методу

Метод getline прочитує з вхідного потоку n-1 символів або менш, якщо

символ переведення рядка зустрінеться раніше, і записує їх в строкову змінну s.

Символ переведення рядка також прочитується (віддаляється) з вхідного потоку,

але не записується в строкову змінну, замість нього розміщується \0. Якщо в

рядку початкових даних більш за n-1 символи, наступне введення

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

Метод get працює аналогічно, але залишає в потоці символ переведення

рядка. У строкову змінну додається \0.

Ніколи не звертайтеся до різновиду методу get з двома аргументами

двічі підряд, не видаливши \n з вхідного потоку. Наприклад:

cin.get(s, n); // 1 - прочитування рядка

cout « s « endl; // 2 - виведення рядка

cin.get(s, n); // 3 - прочитування рядка

cout « s « endl; // 4 - виведення рядка

cin.get(s, n); // 5 - прочитування рядка

cout « s « endl; // 6 - виведення рядка

cout « "Кінець - справі вінець" « endl; // 7

При виконанні цього фрагменту ви побачите на екрані перший рядок,

виведений оператором 2, а потім завершуюче повідомлення, виведене оператором

7. Які б прекрасні рядки ви не ввели з клавіатури в надії, що вони будуть

прочитані операторами 3 і 5, метод get в даному випадку «уткнеться» в символ \n,

залишений у вхідному потоці від першого виклику цього методу (оператор 1). В

результаті будуть лічені і, відповідно, виведені на екран порожні рядки (рядки, що

містять 0 символів). А символ \n так і залишиться «стирчати» у вхідному потоці.

Можливе рішення цієї проблеми — видалити символ \n з вхідного потоку шляхом

виклику методу get без параметрів, тобто після операторів 1 і 3 потрібно вставити

виклик cin.getO

Проте є і простіше рішення — використовувати в таких випадках метод

getline, який після прочитання рядка не залишає у вхідному потоці символ \n.

Якщо в програмі потрібно ввести декілька рядків, метод getline зручно

використовувати в заголовку циклу, наприклад:

Розглянемо тепер способи введення-виведення рядків, що перекочували в

C++ з мови С. По-перше, можна використовувати для введення рядка функцію

scanf, а для виведення— printf, задавши специфікацію формату %s:

#include <stdio.h>

void main()

{

const int n = 10; char s[n];

scanf(“%s", s);

printf(“%s", s);

}

Ім'я рядка, як і будь-якого масиву, є вказівником на його початок, тому

використане в попередніх прикладах застосування функції scanf операція узяття

Page 28: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

28

адреси (&) опущена. Введення виконуватиметься так само, як і для класів

введення- виведення— до першого пробільного символу. Щоб ввести рядок, що

складається з декількох слів, використовується специфікація %с (символи) з

вказівкою максимальної кількості символів, що вводяться, наприклад

scanf(“%10c", s);

Кількість символів може бути тільки цілою константою. При виведенні

можна задати перед специфікацією %s кількість позицій, що відводяться під

рядок: printf("%15s", s);

Рядок при цьому вирівнюється по правому краю відведеного поля. Якщо

задана кількість позицій недостатня для розміщення рядка, вона ігнорується, і

рядок виводиться цілком.

Бібліотека містить також функції, спеціально призначені для введення-

виведення рядків: gets і puts. Попередній приклад з використанням цих функцій

виглядає так:

#include <stdio.h>

void main()

{

const int n = 10;

char s[n];

gets(s); puts(s);

}

Функція gets (s) читає символи з клавіатури до появи символу нового рядка

і поміщає їх в рядок s (сам символ нового рядка в рядок не включається, замість

нього в рядок заноситься нуль-символ). Функція повертає вказівник на рядок s, а

у разі виникнення помилки або кінця файлу — NULL.

Функція puts(s) виводить рядок s на стандартний пристрій виведення,

замінюючи \0 символом нового рядка. Повертає невід’ємне значення при успіху

або EOF при помилці.

Функціями сімейства printf зручніше користуватися в тому випадку, якщо

в одному операторові потрібно ввести або вивести дані різних типів. Якщо ж

робота виконується тільки з рядками, простіше застосовувати спеціальні функції

для введення- виведення рядків gets і puts

Операції з рядками Для рядків не визначена операція присвоєння, оскільки рядок є не

основним типом даних, а масивом. Присвоєння виконується за допомогою

функцій стандартної бібліотеки або посимвольний «уручну» (що менш

переважно, оскільки чревато помилками).

Наприклад, щоб присвоїти рядку р рядок а, можна скористатися функціями

strcpy або strncpy:

char а[100]= "Never trouble trouble";

char *p = new char [m];

strcpy(p, а);

strncpy(p,a, strlen(a)+ 1);

Page 29: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

29

Для використання цих функцій до програми слід підключити заголовний

файл <string.h>.

Функція strcpy (р,а) копіює всі символи рядка, вказаного другим

параметром (а), включаючи \0, в рядок, вказаний першим параметром (р).

Функція strncpy(р, а, n) виконує те ж саме, але не більш за n символи, тобто

число символів, вказаного третім параметром. Якщо нуль-символ в початковому

рядку зустрінеться раніше, копіювання припиняється, а що залишилися до n

символи рядка р заповнюються нуль-символами. Інакше (якщо n менше або рівно

довжині рядка а) завершуючий нуль-символ в р не додається.

Обидві ці функції повертають вказівник на результуючий рядок. Якщо

області пам'яті, займані рядком-призначенням і рядком-джерелом,

перекриваються, поведінка програми не визначена.

Функція strlen( a) повертає фактичну довжину рядка а, не включаючи нуль-

символ.

Програміст винен сам піклуватися про те, щоб в рядку-приймачі вистачило

місця для рядка-джерела (в даному випадку при виділенні пам'яті значення

змінної m повинне бути більше або рівно 100), і про те, щоб рядок завжди мав

завершуючий нуль-символ.

Вихід за межі рядка і відсутність нуль-символа є поширеними причинами

помилок в програмах обробки рядків.

Для перетворення рядка в ціле число використовується функція atoi(str).

Функція перетворить рядок, що містить символьне представлення цілого числа, у

відповідне ціле число. Ознакою кінця числа служить перший символ, який не

може бути інтерпретований як що належить числу. Якщо перетворення не

вдалося, повертає 0.

Аналогічні функції перетворення рядка atol - в довге ціле число (long) і

atof - в дійсне число з подвійною точністю (double).

Приклад застосування функцій перетворення:

char а[] =”10) Зростання - 162 см, вага - 59.5 кг";

int num; long height; double weight;

num = atoi(a);

height = atol(&а[11]);

weight = atof(&а[25]);

cout « num « ' ' « height « ' ' « weight;

Бібліотека надає також різні функції для порівняння рядків і підрядків,

об'єднання рядків, пошуку в рядку символу і підрядка і виділення з рядка лексем.

Робота з символами. Для зберігання окремих символів використовуються змінні типу char. Їх

введення-виведення також може виконуватися як за допомогою класів введення-

виведення, так і за допомогою функцій бібліотеки. При використанні класів

введення-виведення здійснюється як за допомогою операцій переміщення в потік

« і витягання з потоку », так і методів get() і get (char).

Нижче приведений приклад застосування операцій:

#include <iostream.h>

void main()

Page 30: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

30

{

char з, d, e;

cin » з;

cin » d » e;

cout « з « d « e « endl;

}

Символи, що вводяться, можуть розділятися або не розділятися

пробільними символами, тому у такий спосіб ввести символ пропуску не можна.

Для введення будь-якого символу, включаючи пробільні, можна скористатися

методами get() або get(c):

#include <iostream.h>

void main()

{

char з, d, e;

з = cin.get();

cin.get(d); cin.get(e);

cout « з « d « e « endl;

}

Метод get () повертає код витягнутого з потоку символу або EOF, а метод

get (с) записує витягнутий символ в змінну, передану йому як аргумент, а

повертає посилання на потік.

У заголовному файлі <stdio. h> визначена функція getchar() для введення

символу із стандартного введення, а також putchar() для висновку:

#include <stdio.h>

void main()

{

char з, d;

з = getchar(); putchar(c);

d = getchar(); putchar(d);

}

У бібліотеці також визначений цілий ряд функцій, перевіряючих

приналежність символу якій-небудь множині, наприклад безлічі букв (isalfa),

роздільників (isspace), знаків пунктуації (ispunct), цифр (isdigit) і т.д.

Можна передавати рядок у функцію

Динамічні рядки.

У мові С++ існує три способи виділення пам'яті для використовуваних в

програмі даних: автоматичне, статичне і динамічне.

1. Перший спосіб: автоматичне виділення пам'яті.

Найпростіший метод - це оголошення змінних усередині функцій. Якщо

змінна оголошена усередині функції, кожного разу, коли функція викликається,

під змінну автоматично відводиться пам'ять. Коли функція завершується, пам'ять,

займана змінними, автоматично звільняється. Такі змінні називають

автоматичними.

Page 31: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

31

При створенні автоматичних змінних вони ніяк не ініціалізувалися, тобто

значення автоматичної змінної відразу після її створення не визначене, не можна

передбачити як буде це значення. Відповідно, перед використанням автоматичних

змінних їх необхідно або явно ініціалізувати, або присвоїти яке-небудь значення.

int funct ()

{ double f; // значення f невизначене

f = 1.2; // тепер значення f визначене

bool result = true; // явна ініціалізація автоматичної змінної

. }

Аналогічно автоматичним змінним, оголошеним усередині функції,

автоматичні змінні, оголошені усередині блоку (послідовності операторів,

ув’язнених у фігурні дужки) створюються при вході в блок і знищуються при

виході з блоку. У програмі сортування змінна max створюється наново на кожній

ітерації циклу.

Зауваження. Поширеною помилкою є використання адреси автоматичною

змінною після виходу з функції. Конструкція типу:

int*func ()

{ int x;

. . .

return &x;

} дає непередбачуваний результат.

2. Іншим способом виділення пам'яті є статичне.

Якщо змінна визначена зовні функції, пам'ять для неї відводиться статично,

один раз на початку виконання програми, і змінна знищується тільки тоді,

коли програма завершується.

Можна статично виділити пам'ять і під змінну, визначену усередині функції

або блоку. Для цього потрібно використовувати ключове слово static в її

визначенні.

double globalMax;// змінна визначена зовні функції

void func(int x)

{ static bool visited = false; // змінна визначена усередині функції

if (!visited) {. . .

visited = true; // ініціалізація

} . }

У даному прикладі змінна visited створюється на початку виконання

програми. Її початкове значення - false. При першому виклику Функції func умова

в операторові if буде істинною, виконається ініціалізація і змінній visited буде

привласнено значення true. Оскільки статична змінна створюється тільки один

раз, то і її значення зберігаються між викликами функції. При другому і

подальших викликах функції func ініціалізація не проводитиметься. Якби змінна

visited не була оголошена static, то ініціалізація відбувалася б при кожному

виклику функції.

3. Третім способом виділення пам'яті з мові С++ є динамічний.

Page 32: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

32

Пам'ять для величини якого-небудь типа можна виділити, виконавши

операцію new. Як операнд виступає назва типу, а результатом є адреса

виділеної пам'яті.

long* lp;

lp = new long; //создать нове ціле число

Створений таким чином об'єкт існує до тих пір, поки пам'ять не буде явно

звільнена за допомогою операції delete. Як операнд delete повине бути

задана адреса, повернена операцією new:

delete lp;

delete cp;

Динамічний розподіл пам'яті використовується перш за все тоді, коли

наперед не відомо, скільки об'єктів знадобиться в програмі і чи знадобляться вони

взагалі. За допомогою динамічного розподілу пам'яті можна гнучко управляти

часом життя об'єктів, наприклад виділити пам'ять не на самому початку програми

(як для глобальних змінних), але проте зберігати потрібні дані в цій пам'яті до

кінця програми.

Приклад. Динамічно виділяємо пам'ять під рядок змінної довжини і копіюємо

туди початковий рядок.

int length = strlen(src_str); // ст. функція strlen підраховує к-ть

//символов в рядку

char* buffer = new char[length + 1]; // виділити пам'ять і додати один байт для

//завершуючого нульового байту

strcpy(buffer, src_str); // копіювання рядка

Операція new повертає адресу виділеної пам'яті. Проте не гарантовано, що

new завжди завершиться успішно. Об'єм оперативної пам'яті обмежений, і може

трапитися так, що знайти ще одну ділянку вільної пам'яті неможливо. У такому

разі new повертає нульовий вказівник (адреса 0). Результат new необхідно

перевіряти:

char* newstr;

newstr = new char[length] ;

if (newstr == NULL) { // перевірити результат

// обробка помилок

} // пам'ять виділена успішно

Зауваження. Вказівники і динамічний розподіл пам'яті - дуже могутні

засоби мови. З їх допомогою можна розробляти гнучкі і дуже ефективні програми.

Зокрема, одна з областей застосування С++ - системне програмування - практично

було б неможливо без можливості роботи з вказівниками. Проте можливості, які

одержує програміст при роботі з вказівниками, накладають на нього і велику

відповідальність. Найбільша кількість помилок в програму вноситься при роботі з

вказівниками. Як правило, ці помилки є найбільш важкими для виявлення і

виправлення.

Приведемо декілька характерних прикладів помилок.

1. Використання невірної адреси в операції delete. Результат такої операції

Page 33: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

33

непередбачуваний. Цілком можливо, що сама операція пройде успішно, проте

внутрішня структура пам'яті буде зіпсована, що приведе або до помилки в

наступній операції new, або до псування якої-небудь інформації.

2. Пропущене звільнення пам'яті, тобто програма багато разів виділяє пам'ять під

дані, але "забуває" її звільняти. Такі помилки називають витоками пам'яті. По-

перше, програма використовує непотрібну нею пам'ять, тим самим знижуючи

продуктивність. Крім того, цілком можливо що в 99 випадках з 100 програма

успішно виконається. Проте, якщо втрата пам'яті виявиться дуже великою,

програмі не вистачить пам'ять під які-небудь дані і, відповідно, відбудеться

збій.

3. Запис за невірною адресою. Швидше за все будуть зіпсовані які-небудь дані. Як

виявиться така помилка - невірним результатом, збоєм програми або ще яким-

небудь чином - передбачити важко.

Декілька загальних рекомендацій:

1- Використовуйте вказівники і динамічний розподіл пам'яті тільки там, де це

дійсно необхідно. Перевірте, чи можна виділити пам'ять статично або

використовувати автоматичну змінну.

2- Прагніть локалізувати розподіл пам'яті. Якщо який-небудь метод виділяє

пам'ять (особливо під тимчасові дані), він же і повинен її звільнити.

3- Там, де це можливо, використовуйте посилання замість вказівників.

4- Перевіряйте програми за допомогою спеціальних засобів контролю за пам'яттю

(Purify компанії Rational, Bounce Checker компанії Nu-Mega і ін.)

Використання описувача const

У багатьох прикладах ми вже використали ключове слово const для

позначення того, що та або інша величина не змінюється. У даному параграфі

приводяться докладні правила вживання описувача const.

Якщо на початку опису змінної стоїть описувач const, то описуваний

об'єкт не змінюється під час виконання програми:

const double pi = 3.1415;

const Complex one(1,1);

Якщо const стоїть перед визначенням вказівника або посилання, то це

означає, що не змінюється об'єкт на який даний вказівник або посилання указує:

const char* ptr = &string; // вказівник на незмінний рядок

char x = *ptr // звернення по вказівнику - допустимо

ptr++; // зміна вказівника - допустимо

* ptr = 0 // спроба зміни об'єкту, на

// який указує вказівник - помилка

Якщо потрібно оголосити вказівник, значення якого не змінюється, то таке

оголошення виглядає таким чином:

char* const cptr = &string; // незмінний вказівник

char x = *ptr // звернення по вказівнику - допустимо

ptr++; // зміна вказівника - помилка

* ptr = 0 // зміна об'єкту, на який

// вказівник указує - допустимо

Page 34: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

34

Таким чином, використання описувача const дає можливість програмістові

контролювати можливість зміни інформації в програмі, тим самим попереджаючи

можливі помилки.

Зберігання інформації за допомогою файлів.

Заголовний файл iostream.h визначає вихідний потік cout. Аналогічно,

заголовний файл fstream.h визначає клас вихідного файлового потоку з ім'ям

ofstream.

Використовуючи об'єкти класу ofstream, ваші програми можуть виконувати

виведення у файл.

Спершу ви повинні оголосити об'єкт типу ofstream, вказавши ім'я

необхідного вихідного файлу як символьний рядок

ofstream file_object("FILENAME.txt");

Якщо ви указуєте ім'я файлу при оголошенні об'єкту типу ofstream, C++

створить новий файл на вашому диску, використовуючи вказане ім'я, або

перезапише файл з таким же ім'ям, якщо він вже існує на вашому диску.

ЧИТАННЯ З ВХІДНОГО ФАЙЛОВОГО ПОТОКУ

Ваші програми можуть виконати операції введення з файлу,

використовуючи об'єкти типу ifstream. Знову ж таки, ви просто створюєте об'єкт,

передаючи йому як параметр необхідне ім'я файлу:

ifstream input_file("FILENAME.TXT") ;

подібно cin, вхідні файлові потоки використовують порожні символи, щоб

визначити, де закінчується одне значення і починається інше.

Читання цілого рядка файлового введення

Ваші програми можуть використовувати cin.getline для читання цілого

рядка з клавіатури. Так само об'єкти типу ifstream можуть використовувати

getline для читання рядка файлового введення.

ВИЗНАЧЕННЯ КІНЦЯ ФАЙЛУ

Звичайною файловою операцією у ваших програмах є читання вмісту файлу,

поки не зустрінеться кінець файлу.

Щоб визначити кінець файлу, ваші програми можуть використовувати

функцію еоf потокового об'єкту. Ця функція повертає значення 0, якщо кінець

файлу ще не зустрівся, і 1, якщо зустрівся кінець файлу. Використовуючи цикл

while, ваші програми можуть безперервно читати вміст файлу, поки не знайдуть

кінець файлу.

ПЕРЕВІРКА ПОМИЛОК ПРИ ВИКОНАННІ ФАЙЛОВИХ ОПЕРАЦІЙ Наприклад, якщо ви відкриваєте файл для введення, ваші програми повинні

перевірити, що файл існує. Аналогічно, якщо ваша програма пише дані у файл,

вам необхідно переконатися, що операція пройшла успішно (наприклад,

відсутність місця на диску, швидше за все, помішає запису даних). Щоб

допомогти вашим програмам стежити за помилками, ви можете використовувати

функцію fail файлового об'єкту. Якщо в процесі файлової операції помилок не

було, функція поверне не істину (0). Проте якщо зустрілася помилка, функція fail

поверне істину.

ЗАКРИТТЯ ФАЙЛУ, ЯКЩО ВІН БІЛЬШЕ НЕ ПОТРІБНИЙ

Page 35: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

35

При завершенні вашої програми операційна система закриє відкриті нею

файли. Проте, як правило, якщо вашій програмі файл більше не потрібний, вона

повинна його закрити.

Для закриття файлу ваша програма повинна використовувати функцію close

input_file.close();

Коли ви закриваєте файл, всі дані, які ваша програма писала в цей файл,

скидаються на диск, і оновлюється запис каталога для цього файлу.

УПРАВЛІННЯ ВІДКРИТТЯМ ФАЙЛУ

У розглянутому вище, файлові операції введення і висновку виконувалися з

початку файлу. Проте, коли ви записуєте дані у вихідний файл, ймовірно, ви

захочете, щоб програма додавала інформацію в кінець існуючого файлу.

Для відкриття файлу в режимі додавання ви повинні при його відкритті

вказати другою параметр,ifstream output_file("FILENAME.TXT", ios::app);

В даному випадку параметр ios::app вказує режим відкриття файлу.

У міру ускладнення ваших програм вони використовуватимуть поєднання

значень для режиму відкриття файлу, які перераховані в табл.

Режим відкриття Призначення

ios::app Відкриває файл в режимі додавання, розташовуючи

файловий вказівник в кінці файлу.

ios::ate Розташовує файловий вказівник в кінці файлу.

ios::in Вказує відкрити файл для введення.

ios::nocreate Якщо вказаний файл не існує, не створювати файл

і повернути помилку.

ios::noreplace Якщо файл існує, операція відкриття повинна бути

перервана і повинна повернути помилку.

ios::out Вказує відкрити файл для виведення.

ios::trunc Скидає (перезаписує) вміст існуючого файлу.

Наступна операція відкриття файлу відкриває файл для виведення,

використовуючи режим ios::noreplace, щоб запобігти перезапису існуючого файлу

ifstream output_file("FILENAME.TXT", ios::out | ios::noreplace);

ВИКОНАННЯ ОПЕРАЦІЙ ЧИТАННЯ І ЗАПИСУ

Всі програми, представлені в даному уроці, виконували файлові операції над

символьними рядками.

У міру ускладнення ваших програм, можливо, вам знадобиться читати і

писати масиви і структури. Для цього ваші програми можуть використовувати

функції read і write. При використанні функцій read і write ви повинні вказати

буфер даних, в який дані читатимуться або з якого вони записуватимуться, а також

довжину буфера в байтах.

input_file.read(buffer, sizeof(buffer));

output_file.write(buffer, sizeof(buffer));

Page 36: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

36

Змістовий модуль 3. Структури. Файли. Таблиці ідентифікаторів.

Основні розділи теми.

1. Зберігання зв'язаної інформації в структурах.

2. Об’єднання.

3. Бітові операції, бітові поля.

4. Робота з абстрактними типами даних: struct.

5. Використання бітових операцій.

6. Бінарні файли.

7. Призначення таблиць ідентифікаторів.

8. Принципи організації таблиць ідентифікаторів

9. Способи організації таблиць ідентифікаторів

10. Побудова таблиць ідентифікаторів за методом бінарного дерева

11. Пошук елемента в дереві

12. Хеш-функції і хеш-адресація

13. Хеш-адресація з рехешируванням

14. Хеш-адресація з використанням методу ланцюжків

15. Алгоритм методу ланцюжків

16. Комбіновані способи побудови таблиць ідентифікаторів

Питання для самоконтролю.

1. Що таке структура?

2. Оголошення структури.

3. Як можна звертатися до окремих частин структури ?

4. Вказівники на структури.

5. Оператор стрілка (->).

6. Бітові операції: побітове заперечення, побітове зрушення вліво,

побітове зрушення управо, побітове множення, побітове складання,

що виключає, побітове складання

7. Що таке об'єднання? Як оголошується? Анонімне об'єднання.

8. Бітові поля.

9. Бінарні файли.

10. Перевага бінарних файлів.

11. Стандартна функція сортування qsort.

12. Чотири параметри функції qsort.

13. Література: 9 с. 67-72, с. 282-.284, с. 238 4 с. 127-158, с. 268-278.

Зберігання зв'язаної інформації в структурах.

Об’єднання. Бітові операції, бітові поля. Робота з абстрактними типами

даних: struct. Використання бітових операцій.

Page 37: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

37

Структура - це складений тип даних, побудований з використанням

інших типів. Структура складається з полів. Поля (елементи структури) - змінні або

масиви стандартного типу (int, char і т.п) або інші, раніше описані структури.

Описаний тип даних можна використовувати для оголошення змінних типу

структур.

Оголошення структури здійснюється за допомогою ключового слова struct,

за яким йде її ім'я і далі список елементів, увязнених у фігурні дужки:

struct [ім'я] {

тип_елементу_1 і’мя_елементу_1;

тип_елементу_2 і’мя_елементу_2;

...

тип_елементу_n і’мя_елементу_n;

};

Ім'я структури використовується при оголошенні змінних даного типу. Ім'ям

елементу може бути будь-який ідентифікатор. Через кому можна записувати

декілька ідентифікаторів одного типу. Елементи однієї і тієї ж структури повинні

мати унікальні імена, але дві різні структури можуть містити елементи з

однаковими іменами. Кожне визначення структури повинне закінчуватися

крапкою з комою. Наприклад

struct date { int day, month, year; };

або

struct date {

int day;

int month;

int year;

};

Елементи структури можуть бути будь-якого типу і одна структура може

містити елементи багатьох різних типів. Структура не може, проте, містити

екземпляри самої себе. Наприклад, елемент типу date не може бути оголошений у

визначенні структури date

struct date {

int day;

int month;

int year;

date last_date; // помилка!

Page 38: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

38

};

Слідом за фігурною дужкою, що закінчує список елементів, можуть

записуватися змінні даного типу, наприклад:

struct date { ... } а, b, з; (при цьому виділяється відповідна пам'ять під

змінні а, b і з).

Опис без подальшого списку не резервує ніякого простору в пам'яті; визначення

тільки створює новий тип даних, який використовується для оголошення змінних.

Змінні структури оголошуються так само, як змінні інших типів. Наприклад

date days;

або

struct date days; (запис в стилі Сі)

Тепер змінна days має тип date.

При необхідності структури можна ініціалізувати, поміщаючи за

оголошенням список початкових значень елементів.

struct POINT { // оголошення структури POINT

int x; // визначення елементів структури x і у

int у;

} spot = { 20, 40 }; // Змінна spot має значення x = 20, у = 40

Поля структури можуть бути самі структурами, тобто дозволяється вкладати

структури одна в іншу, наприклад:

struct Man {

char name [30], fam [20];

struct date bd;

int age;

};

Тут name і fam - символьні масиви, відповідно розміром 30 і 20 байт. Змінна

bd представлена складовим елементом типу date, змінна age містить значення

цілого типу.

Як видно із специфікації, не обов'язково указувати ім'я структури. Таке

визначення структури називається анонімним. Наприклад

struct {

char abbreviation[8];

char fullname[60];

} title;

До окремих частин структури можна звертатися через складене ім'я. Формат

звернення:

имя_структуры.имя_поля

Щоб звернутися до окремого елементу структури, необхідно вказати її ім'я,

поставити крапку і відразу за нею написати ім'я потрібного елементу.

Наприклад

int а;

struct date birthday; /*объявление змінної birthday типу date

рівнозначно оголошенню date birthday;*/

а = birthday.month; /* змінної а привласнюється

Page 39: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

39

значення поля month змінної birthday*/

birthday.year = 2001; /* полю year змінної birthday

присвоюється число 2001*/

Оскільки структура - це складений тип, існують деякі особливості їх

використання. Наприклад, структури не можуть бути виведені на екран або

введені з клавіатури як єдине ціле, а тільки по одному елементу, не можна

порівнювати структури в цілому, їх треба порівнювати елемент за елементом,

проте дозволяється присвоювати одну структуру іншій по їх іменах.

Ім'я структури володіє всіма правами імен типів, а отже, можна визначати

вказівники на структури:

ім'я_структури *ім'я_вказівника на структуру;

Як завжди, вказівник можна проініціалізувати. Значенням вказівника на

структуру може бути адреса структури того ж типу, тобто адреса байта,

починаючи з якого структура розміщується в пам'яті. Структура задає розмір

об'єкту і тим самим визначає, на яку величину (на скільки байт) зміниться

значення вказівника на структуру, якщо до нього додати (відняти) 1.

Наприклад

// оголошення структури Date

struct Date {

int day;

int month;

int year;

};

void main()

{

Date my_date *birthday; // визначення змінної my_date типу Date

оголошення вказівника на об'єкт типу Date

birthday = &my_date; //ініціалізація вказівника birthday адресою структури

my_date

}

У попередніх розділах для доступу до елементів структури ми

використовували операцію крапка (.). Операція крапка звертається до елементу

структури по імені змінної об'єкту. Але, якщо у нас визначений вказівник на

структуру, то з'являється ще одна можливість доступу до елементів структури. Її

забезпечує оператор стрілка (->). Формат відповідного виразу наступний:

ім'я_вказівника->ім'я_елемента_структури

Оператор стрілка, що складається із знаку мінус (-) і знаку більше (>),

записаних без пропуску, забезпечує доступ до елементів структури через

вказівник на об'єкт.

Наприклад, продовження прикладу

/* використовуємо операцію крапка, щоб ініціалізувати елементи структури

my_date */

my_date.year = 2001;

my_date.month = 2;

my_date.day = 10;

Page 40: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

40

//використовуємо операцію крапка, щоб вивести на екран елементи структури

my_date

cout << "The date is "<<my_date.day<<"/"<< my_date.month<< "/"<<my_date.year<<

endl;

//використовуємо операцію стрілка, щоб вивести на екран елементи структури

my_date через вказівник

birthday

cout << "My birthday is " << birthday->day<< "." << birthday->month << endl;

Вираз birthday->month еквівалентний (*birthday).month,

який «розіменує» вказівник і робить доступним елемент month через оператор

крапка. Дужки потрібні, оскільки оператор крапка має вищий пріоритет, ніж

оператор «розіменування» вказівника (*).

Як і для інших об'єктів, для структур можуть бути визначені посилання:

і'мя_структури &і'мя_посилання_на_структуру ініціалізатор;

Наприклад, для змінної my_date можна, так ввести посилання:

Date &now = my_date;

Після такого визначення now є синонімом імені пременной my_date. Для

доступу до елементів структури використовується оператор крапка.

Таким чином, наступні чотири вирази еквівалентні і забезпечують доступ до

елементу month змінної my_date типу Date.

my_date.month //по імені змінної

now.month // по посиланню

birthday->month // через вказівник

(*birthday).month // через «розіменованний» вказівник

Бітові операції

Бітових операцій всього 6 (приводяться з урахуванням пріоритету):

~ - побітове заперечення

<< - побітове зрушення вліво

>> - побітове зрушення вправо

& - побітове множення

^ - побітове додавання, що виключає (складання по модулю)

| - побітове складання

Побітове заперечення

~ - побітове заперечення працює таким чином: кожен біт числа змінює свій стан

на протилежний, тобто нуль стає одиницею, а одиниця нулем.

Приклад:

Якщо застосувати цю операцію до двійкового числа 01010110 (приклади на

двійкових числах наочніші), то ~010101102 = 101010012

У графіці ця операція називається інверсією.

Побітове зрушення вліво

<< - при побітовому зрушенні вліво кожен біт числа зрушується на задану

кількість розрядів (N розрядів) вліво. При цьому в результуючому числі N

старших розрядів втрачається, а в N молодших розрядах з'являються нулі. Якщо

Page 41: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

41

правий операнд містить від’ємне значення або його значення >= кількості біт в

лівому операнді, то результат операції не визначений. Результат має тип лівого

операнда. Дана операція найчастіше застосовується для переміщення бітів в

задану позицію.

Приклад:

100111012 << 3 = 111010002

Побітове зрушення управо

>> - при побітовому зрушенні управо кожен біт числа зрушується на задану

кількість розрядів (N розрядів) управо. При цьому в результуючому числі N

молодших розрядів втрачається, а в N старших розрядах з'являються нулі, якщо

лівий операнд містить додатнє значення, або в N старших розрядах з'являються

одиниці (справедливо для Microsoft Visual C++, але не обов'язково для інших

платформ), якщо лівий операнд містить від’ємне значення. Якщо правий операнд

містить від’ємне значення або його значення >= кількості біт в лівому операнді, то

результат операції не визначений. Результат має тип лівого операнда. Ця операція

найчастіше застосовується для переміщення бітів в задану позицію.

Приклад:

010111012 >> 3 = 000010112

100111012 >> 3 = 111100112

Побітове множення

& - при побітовому множенні кожен біт одного числа множиться на

відповідний йому біт (що знаходиться в тій же позиції) іншого числа. При цьому в

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

біти обох чисел в цій позиції були рівні одиницям, інакше відповідний біт

результуючого числа буде рівний нулю.

Приклад:

Дано двійкове число 10011101, необхідно виділити його старшу частину.

Для цього дане число необхідно побітово помножити на число, що містить в

старшій частині одиниці, а в молодшій - нулі (таким чином число 111100002 є

маскою).

100111012 & 111100002 = 100100002Побітове додавання, що виключає

^ - при побітовому додавання (ця операція називається складанням по

модулю або XOR), що виключає, кожен біт одного числа складається з

відповідним йому бітом (що знаходиться в тій же позиції) іншого числа. При

цьому в результуючому числі відповідний біт буде рівний одиниці тільки в тому

разі якщо біти обох чисел в цій позиції були різні (0 і 1 або 1 і 0), інакше (якщо

біти мають однакові значення: 0 і 0 або 1 і 1) відповідний біт результуючого числа

буде рівний нулю.

Приклад:

Дано двійкове число 10000101, необхідно перемкнути його непарні біти (1-

ою, 3-ою, 5-ою, 7-ою). Для цього дане число необхідно скласти по модулю з

числом, що містить одиниці в заданих розрядах.

100001012 ^ 101010102 = 001011112

Операція XOR також широко використовується в багатьох алгоритмах

шифрування інформації, оскільки ця операція володіє властивістю оборотності. Це

Page 42: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

42

означає, що якщо над результатом приведеного вище прикладу повторно провести

ту ж операцію, то результатом буде початкове число.

001011112 ^ 101010102 = 100001012

Число 101010102 в даному випадку називатиметься ключем шифрування.

Побітове додавання

| - при побітовому додаванні кожен біт одного числа складається з

відповідним йому бітом (що знаходиться в тій же позиції) іншого числа. При

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

якщо біти обох чисел в цій позиції були рівні нулю, інакше відповідний біт

результуючого числа буде рівний одиниці.

Приклад:

Дано двійкове число 10000101, необхідно виставити його непарні біти (1-

ою, 3-ою, 5-ою, 7-ою) в одиницю. Для цього дане число необхідно побітово

скласти з числом, що містить одиниці в заданих розрядах.

100001012 | 101010102 = 101011112

Об'єднання

Об'єднання - це формат даних, який може містити різні типи даних, але

тільки один тип одночасно. Об'єднання дозволяє декільком змінним різних типів

займати одну ділянку пам'яті (тобто всі змінні-члени об'єднання мають однакову

адресу).

Синтаксис об'єднання наступний:

union [і'мя_об'єднання] {список_змінних} [і'мя_об'єкту] ;

union Num{

int int_var;

double double_var;

};

Розмір, який займає в пам'яті об'єднання, рівний розміру найбільшого

елементу даного об'єднання

Тобто, коли ми створюємо об'єднання, компілятор виділяє пам'ять під

об'єднання розміром з найбільший елемент об'єднання. Надалі в дану ділянку

пам'яті записуються значення елементів об'єднання починаючи з початкового

байта цієї ділянки. Таким чином, кожне нове значення "затирає" старе, навіть

якщо це значення іншого елементу. Саме тому можна працювати тільки з одним

елементом об'єднання.

У С++ не обов'язково використовувати ключове слово union, тобто об'єкт

об'єднання можна оголосити таким чином:

і'мя_об'єднання і'мя_об'єкта;

Існує таке поняття як анонімне об'єднання, тобто об'єднання, яке не має

ніякого імені. Елементи такого об'єднання стають по суті повноцінними змінними,

Page 43: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

43

які розділяють одну адресу, але все також в одне і теж час поточним може бути

тільки один елемент.

Присвоєння значень елементам анонімного об'єднання відбувається точно

також як і присвоєння значень звичайним змінним. Якщо анонімне об'єднання

оголошене в глобальному контексті, то воно повинне бути визначене як статичне.

Бітові поля

Бітові поля - це своєрідна структура, яка дозволяє працювати з окремими

бітами.

Синтаксис оголошення бітових полів наступний:

struct [і'мя_структури] {

тип [і'мя_бітового_поля1]: довжина;

тип [і'мя_бітового_поля2]: довжина;

. . . . .

тип [і'мя_бітового_поляN]: довжина;

} [і'мя_об'єкта];

У С++ дозволено використовувати окрім перерахованих вище будь-який

тип, що інтерпретується як цілий: char, bool, short, long і переліки. Довжина

бітового поля задається цілочисельним значенням, яке визначає, скільки бітів

необхідно виділити вказаному полю.

Розмір одного бітового поля не повинен перевищувати розмір типу даного

поля. Тобто, якщо бітове поле оголошене як unsigned або int, то розмір такого

поля не повинен перевищувати 32 бита. Посилання на бітове поле виконується по

імені_бітового_поля. Якщо ім'я поля не вказане, запитані біти все одно

виділяються, але доступ до них неможливий. Таке поле називається

неіменованим бітовим полем.

struct Example1 {

unsigned n1: 6;

unsigned : 2;

unsigned n2: 14;

unsigned : 2;

unsigned n3: 5;

};

До бітових полів не може бути застосована операція отримання адреси (&) і

тому не існує вказівників на поля.

Бітові поля можна використовувати для створення всіляких масок і

прапорів, а також при створенні двійково-упакованих об'єктів.

Бінарні файли.

Сортування масиву структур.

Бінарні файли, це файли, в яких інформація зберігається у

внутрішній формі представлення.

Page 44: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

44

Перевага бінарних файлів полягає в тому, що, по-перше, при

читанні/записі не витрачається час на перетворення даних з символьної форми

представлення у внутрішню і назад, а по-друге, при цьому не відбувається

втрати точності дійсних чисел. Крім того, при роботі з бінарними файлами

широко застосовується прямий доступ до інформації шляхом установки

поточної позиції вказівника. Це дає можливість швидкого отримання і зміни

окремих даних файлу. Бінарний файл відкривається в двійковому режимі, а

читання/запис в нього виконуються за допомогою функцій бібліотеки fread і

fwrite.

Для формування записів в бінарному файлі тут застосовується функція

fwrite:

size_t fwrite(const void *p, size_t size. size_t n, FILE *f)

Вона записує n елементів завдовжки size байт з буфера, заданого

вказівником р, в потік f. Повертає число записаних елементів.

Для читання з бінарного файлу в другій програмі застосовуватимемо

функцію fread:

size_t fread(void *p, size_t size, size_t n. FILE *f);

Вона прочитує n елементів завдовжки size байт в буфер, заданий

вказівником р, з потоку f. Повертає число лічених елементів, яке може бути

менше, ніж потрібне.

Тип size_t є беззнаковим цілочисельним типом, використовуваним для

представлення результату sizeof.

#include <stdio.h>

FILE *fin; Тип FILE містить інформацію, необхідну для виконання

операції з файлом.

Стандартна функція сортування qsort.

Її прототип знаходиться в заголовному файлі <stdlib.h>. Функція може

виконувати сортування масивів будь-яких розмірів і типів. У неї чотири

параметри:

1. вказівник на початок області, в якій розміщується упорядковувана

інформація;

2. кількість сортованих елементів;

3. розмір кожного елементу в байтах;

4. ім'я функції, яка виконує порівняння двох елементів.

Функція qsort універсальна, ми повинні дати їй інформацію, як порівнювати

сортовані елементи і які висновки робити з порівняння. Значить, ми повинні самі

написати функцію, яка порівнює два будь-яких елементи, і передати її в qsort. Для

правильної роботи qsort потрібно, щоб ця функція мала два параметри —

вказівники на порівнювані елементи. Вони повинні мати тип void. Функція

повинна повертати значення, менше нуля, якщо перший елемент менше другого,

рівне нулю, якщо вони рівні, і більше нуля, якщо перший елемент більший. При

цьому масив буде впорядкований за збільшенням. Якщо ми хочемо упорядкувати

масив по убуванню, потрібно змінити повертані значення так: якщо перший

елемент менше другого, повертати значення, більше нуля, а якщо більше —

менше.

Page 45: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

45

Усередині функції треба привести вказівники на void до типу вказівника на

структуру наприклад Man. Для цього можна використовувати операцію

приведення типу в стилі С (Man *).

Або застосовувати для цього операцію reinterpret_cast, введену в стандарт

C++ відносно недавно. Наприклад, функція порівняння з використанням

reinterpret_cast виглядає ось таким чином:

int compare(const void *man1, const void *man2)

{

return strcmp((reinterpret_cast <const Man *> (man1))->name,

(reinterpret_cast <const Man *> (man2))->name);

}

Щоб опис структури був відомий у функції compare, потрібно перенести

опис структури, а разом з нею і опис необхідної для неї константи l_name в

глобальну область. Для впорядковування масиву по іншому полю треба змінити

функцію порівняння. Ось, наприклад, як вона виглядає при сортуванні за

збільшенням року народження:

int compare(const void *man1, const void *man2)

{

int p;

if(((Man *) man1)->birth_year < ((Man *) man2)->birth_year) p = -1;

else if(((Man*)man1)->birth_year ==((Man*)man2)->birth_year)

p = 0; else p = 1;

return p;

}

Можна записати те ж саме компактніше за допомогою тернарной умовної

операції. Для різноманітності приведемо функцію для сортування по убуванню

окладу:

int compare(const void *man1, const void *man2)

{

return ((Man *) man1)->pay > ((Man *) man2)->pay ? -

1:

((Man *) man1)->pay ==((Man *) man2)->pay ? 0 : 1;

}

Таблиці ідентифікаторів.

Призначення таблиць ідентифікаторів

При виконанні семантичного аналізу, генерації коду і оптимізації

результуючої програми компілятор повинен оперувати характеристиками

основних елементів вихідної програми - змінних, констант, функцій та інших

лексичних одиниць вхідної мови. Ці характеристики можуть бути отримані

компілятором на етапі синтаксичного аналізу вхідної програми (найчастіше при

аналізі структури блоків описів змінних і констант), а також доповнені на етапі

підготовки до генерації коду (наприклад при розподілі пам'яті).

Набір характеристик, відповідний кожному елементу вихідної програми,

залежить від типу цього елемента, від його змісту (семантики) і, відповідно, від

Page 46: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

46

тієї ролі, яку він виконує в вихідної і результуючої програмах. У кожному

конкретному випадку цей набір характеристик може бути свій залежно від

синтаксису і семантики вхідної мови, від архітектури цільової обчислювальної

системи і від структури компілятора. Але є типові характеристики, які найчастіше

притаманні тим чи іншим елементам вихідної програми. Наприклад для змінної -

це її тип і адресу осередки пам'яті, для константи - її значення, для функції -

кількість і типи формальних аргументів, тип повертається результату, адреса

виклику коду функції.

Головною характеристикою будь-якого елементу вихідної програми є його

ім'я. Саме з іменами змінних, констант, функцій і інших елементів вхідного мови

оперує розробник програми - тому і компілятор повинен вміти аналізувати ці

елементи за їхніми іменами.

Ім'я кожного елемента має бути унікальним. Багато сучасні мови

програмування допускають збіги (неунікальність) імен змінних і функцій в

залежності від їх області видимості та інших умов вихідної програми. В цьому

випадку унікальність імен повинен забезпечувати сам компілятор, - імена

елементів вихідної програми завжди є унікальними.

Таким чином, завдання компілятора полягає в тому, щоб зберігати деяку

інформацію, пов'язану з кожним елементом вихідної програми, і мати доступ до

цієї інформації на ім'я елемента. Для вирішення цього завдання компілятор

організовує спеціальні сховища даних, звані таблицями ідентифікаторів, або

таблицями символів. Таблиця ідентифікаторів складається з набору полів даних

(записів), кожне з яких може відповідати одному елементу вихідної програми.

Запис містить всю необхідну компілятору інформацію про даному елементі і

може поповнюватися в міру роботи компілятора. Кількість записів залежить від

способу організації таблиці ідентифікаторів, але в будь-якому випадку їх не може

бути менше, ніж елементів у вихідній програмі. В принципі, компілятор може

працювати не з однією, а з кількома таблицями ідентифікаторів - їх кількість і

структура залежать від реалізації компілятора.

Принципи організації таблиць ідентифікаторів

Компілятор поповнює записи в таблиці ідентифікаторів у міру аналізу

вихідної програми і виявлення в ній нових елементів, які потребують розміщення

в таблиці. Пошук інформації в таблиці виконується всякий раз, коли компілятору

необхідні відомості про той чи інший елемент програми. Причому слід зауважити,

що пошук елемента в таблиці буде виконуватися компілятором істотно частіше,

ніж приміщення в неї нових елементів. Так відбувається тому, що опису нових

елементів у вихідній програмі, як правило, зустрічаються набагато рідше, ніж ці

елементи використовуються. Крім того, кожному додаванню елемента в таблицю

ідентифікаторів в будь-якому випадку буде передувати операція пошуку - щоб

переконатися, що такого елемента в таблиці немає.

На кожну операцію пошуку елемента в таблиці компілятор буде витрачати

час, і оскільки кількість елементів у вихідній програмі велике (від одиниць до

сотень тисяч в залежності від обсягу програми), цей час буде істотно впливати на

загальний час компіляції. Тому таблиці ідентифікаторів повинні бути організовані

Page 47: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

47

таким чином, щоб компілятор мав можливість максимально швидко виконувати

пошук потрібної йому записи таблиці на ім'я елемента, з яким пов'язана ця запис.

Можна виділити наступні способи організації таблиць ідентифікаторів:

• прості і впорядковані списки;

• бінарне дерево;

• хеш-адресація з рехешірованіем;

• хеш-адресація за методом ланцюжків;

• комбінація хеш-адресації зі списком або бінарним деревом.

Найпростіші методи побудови таблиць ідентифікаторів

У найпростішому випадку таблиця ідентифікаторів є лінійний

невпорядкований список, або масив, кожна осередок якого містить дані про

відповідний елементі таблиці. Розміщення нових елементів в такій таблиці

виконується шляхом запису інформації в черговий осередок масиву або списку у

міру виявлення нових елементів у вихідній програмі.

Пошук потрібного елемента в таблиці буде в цьому випадку виконуватися

шляхом послідовного перебору всіх елементів і порівняння їх імені з ім'ям

шуканого елемента, поки не буде знайдений елемент з таким же ім'ям. Тоді якщо

за одиницю часу прийняти час, що витрачається компілятором на порівняння двох

рядків (в сучасних обчислювальних системах таке порівняння найчастіше

виконується однією командою), то для таблиці, що містить N елементів, в

середньому буде виконано N / 2 порівнянь. Час, необхідний на додавання нового

елемента в таблицю (Tд), не залежить від числа елементів в таблиці (N). Але якщо

N велике, то пошук зажадає значних витрат часу. Час пошуку (Tп) в такій таблиці

можна оцінити як Tп = O (N). Оскільки саме пошук в таблиці ідентифікаторів є

найбільш часто виконуваної компілятором операцією, такий спосіб організації

таблиць ідентифікаторів є неефективним. Він застосовується лише для

найпростіших компіляторів, які працюють з невеликими програмами.

Пошук може бути виконаний більш ефективно, якщо елементи таблиці

відсортовані (впорядковані) природним чином. Оскільки пошук здійснюється по

імені, найбільш природним рішенням буде розташувати елементи таблиці в

прямому або зворотному алфавітному порядку. Ефективним методом пошуку в

упорядкованому списку з N елементів є бінарний, або логарифмічний, пошук.

Алгоритм логарифмічного пошуку полягає в наступному: шуканий символ

порівнюється з елементом (N + 1) / 2 в середині таблиці; якщо цей елемент не є

шуканим, то ми повинні переглянути тільки блок елементів, пронумерованих від 1

до (N + 1) / 2 - 1, або блок елементів від (N + 1) / 2 + 1 до N в залежності від того,

менше або більше шуканий елемент того, з яким його порівняли. Потім процес

повторюється над потрібним блоком в два рази меншого розміру. Так триває до

тих пір, поки або шуканий елемент не буде знайдений, або алгоритм не дійде до

чергового блоку, що містить один або два елементи (з якими можна виконати

пряме порівняння шуканого елемента).

Так як на кожному кроці число елементів, які можуть містити шуканий

елемент, скорочується в два рази, максимальне число порівнянь одно 1 + log2 N.

Тоді час пошуку елемента в таблиці ідентифікаторів можна оцінити як Tп = O

(log2 N). Для порівняння: при N = 128 бінарний пошук вимагає щонайбільше 8

Page 48: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

48

порівнянь, а пошук в невпорядкованою таблиці - в середньому 64 порівняння.

Метод називають «бінарним пошуком», оскільки на кожному кроці обсяг даної

інформації скорочується в два рази, а «логарифмическим» - оскільки час, що

витрачається на пошук потрібного елемента в масиві, має логарифмічну

залежність від загальної кількості елементів в ньому.

Недоліком логарифмічного пошуку є вимога упорядкування таблиці

ідентифікаторів. Так як масив інформації, в якому виконується пошук, повинен

бути впорядкований, час його заповнення вже буде залежати від числа елементів в

масиві. Таблиця ідентифікаторів часто проглядається компілятором ще до того, як

вона заповнена, тому потрібно, щоб умова впорядкованості виконувалося на всіх

етапах обігу до неї. Отже, для побудови такої таблиці можна користуватися тільки

алгоритмом прямого упорядкованого включення елементів.

Якщо користуватися стандартними алгоритмами, застосовуваними для

організації впорядкованих масивів даних, то середній час, необхідний на

приміщення всіх елементів в таблицю, можна оцінити таким чином:

Тут k - деякий коефіцієнт, що відображає співвідношення між часом,

затраченими комп'ютером на виконання операції порівняння і операції

перенесення даних.

При організації логарифмічного пошуку в таблиці ідентифікаторів

забезпечується істотне скорочення часу пошуку потрібного елемента за рахунок

збільшення часу на приміщення нового елемента в таблицю. Оскільки додавання

нових елементів у таблицю ідентифікаторів відбувається значно рідше, ніж

звернення до них, цей метод слід визнати більш ефективним, ніж метод організації

невпорядкованою таблиці. Однак в реальних компіляторах цей метод

безпосередньо також не використовується, оскільки існують більш ефективні

методи.

Побудова таблиць ідентифікаторів за методом бінарного дерева

Можна скоротити час пошуку шуканого елемента в таблиці ідентифікаторів,

не збільшуючи значно час, необхідний на її заповнення. Для цього треба

відмовитися від організації таблиці у вигляді безперервного масиву даних. Існує

метод побудови таблиць, при якому таблиця має форму бінарного дерева. Кожен

вузол дерева являє собою елемент таблиці, причому кореневих вузлом стає

перший елемент, зустрінутий компілятором при заповненні таблиці. Дерево

називається бінарним, так як кожна вершина в ньому може мати не більше двох

гілок. Для визначеності будемо називати дві гілки «права» і «ліва».

Розглянемо алгоритм заповнення бінарного дерева. Будемо вважати, що

алгоритм працює з потоком вхідних даних, що містить ідентифікатори. Перший

ідентифікатор, як уже було сказано, поміщається в вершину дерева. Всі подальші

ідентифікатори потрапляють в дерево за наступним алгоритмом:

1. Вибрати черговий ідентифікатор з вхідного потоку даних. Якщо чергового

ідентифікатора немає, то побудова дерева закінчено.

2. Зробити поточним вузлом дерева кореневу вершину.

Page 49: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

49

3. Порівняти ім'я чергового ідентифікатора з ім'ям ідентифікатора, що

міститься в поточному вузлі дерева.

4. Якщо ім'я чергового ідентифікатора менше, то перейти до кроку 5, якщо

дорівнює припинити виконання алгоритму (двох однакових ідентифікаторів бути

не повинно!), інакше - перейти до кроку 7.

5. Якщо у поточного вузла існує ліва вершина, то зробити її поточним вузлом

і повернутися до кроку 3, інакше - перейти до кроку 6.

6. Створити нову вершину, помістити в неї інформацію про чергове

ідентифікатор, зробити цю нову вершину лівої вершиною поточного вузла і

повернутися до кроку 1.

7. Якщо у поточного вузла існує права вершина, то зробити її поточним

вузлом і повернутися до кроку 3, інакше - перейти до кроку 8.

8. Створити нову вершину, помістити в неї інформацію про чергове

ідентифікатор, зробити цю нову вершину правої вершиною поточного вузла і

повернутися до кроку 1.

Розглянемо як приклад послідовність ідентифікаторів Ga, D1, М22, Е, А12,

ВС, F. На рис. 1 проілюстровано весь процес побудови бінарного дерева для цієї

послідовності ідентифікаторів.

Рисунок. 1. Заповнення бінарного дерева для послідовності ідентифікаторів.

Пошук елемента в дереві виконується за алгоритмом, схожим з алгоритмом

заповнення дерева:

1. Зробити поточним вузлом дерева кореневу вершину.

2. Порівняти ім'я шуканого ідентифікатора з ім'ям ідентифікатора, що

містяться в поточному вузлі дерева.

3. Якщо імена співпадають, то шуканий ідентифікатор знайдений, алгоритм

завершується, інакше треба перейти до кроку 4.

4. Якщо ім'я чергового ідентифікатора менше, то перейти до кроку 5, інакше -

перейти до кроку 6.

5. Якщо у поточного вузла існує ліва вершина, то зробити її поточним вузлом

і повернутися до кроку 2, інакше - шуканий ідентифікатор не знайдений, алгоритм

завершується.

Page 50: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

50

6. Якщо у поточного вузла існує права вершина, то зробити її поточним

вузлом і повернутися до кроку 2, інакше - шуканий ідентифікатор не знайдений,

алгоритм завершується.

Для даного методу число необхідних порівнянь і форма отриманого дерева

залежать від того порядку, в якому надходять ідентифікатори. Наприклад, якщо в

розглянутому вище прикладі замість послідовності ідентифікаторів Ga, D1, М22,

Е, А12, ВС, F взяти послідовність А12, ВС, D1, Е, F, Ga, М22, то дерево

виродиться в упорядкований односпрямований зв'язний список. Ця особливість є

недоліком даного методу організації таблиць ідентифікаторів. Іншими недоліками

методу є: необхідність зберігати дві додаткові посилання на ліву і праву гілки в

кожному елементі дерева і робота з динамічним виділенням пам'яті при побудові

дерева.

Якщо припустити, що послідовність ідентифікаторів у вихідній програмі є

статистично невпорядкованою (що в цілому відповідає дійсності), то можна

вважати, що побудоване бінарне дерево буде невиродженим. Тоді середній час на

заповнення дерева (Тд) і на пошук елемента в ньому (Тп) можна оцінити таким

чином:

Незважаючи на зазначені недоліки, метод бінарного дерева є досить вдалим

механізмом для організації таблиць ідентифікаторів. Він знайшов своє

застосування в ряді компіляторів. Іноді компілятори будують кілька різних дерев

для ідентифікаторів різних типів і різної довжини.

Хеш-функції і хеш-адресація

У реальних вихідних програмах кількість ідентифікаторів настільки велике,

що навіть логарифмічну залежність часу пошуку від їх числа не можна визнати

задовільною. Необхідні більш ефективні методи пошуку інформації в таблиці

ідентифікаторів. Кращих результатів можна досягти, якщо застосувати методи,

пов'язані з використанням хеш-функцій і хеш-адресації.

Хеш-функцією F називається деяке відображення множини вхідних елементів

R на множину цілих невід'ємних чисел Z:

Сам термін «хеш-функція» походить від англійського терміна «hash function»

(hash - «заважати», «змішувати», «плутати»).

Безліч допустимих вхідних елементів R називається областю визначення хеш-

функції. Безліччю значень хеш-функції F називається підмножина М з множини

цілих невід'ємних чисел Z:

що містить всі можливі значення, що повертаються функцією F:

Процес відображення області визначення хеш-функції на безліч значень

називається хешированнням.

Page 51: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

51

При роботі з таблицею ідентифікаторів хеш-функція повинна виконувати

відображення імен ідентифікаторів на множину цілих невід'ємних чисел. Областю

визначення хеш-функції буде безліч всіх можливих імен ідентифікаторів.

Хеш-адресація полягає у використанні значення, що повертається хеш-

функцією, як адресу комірки з деякого масиву даних. Тоді розмір масиву даних

повинен відповідати області значень використовуваної хеш-функції. Отже, в

реальному компіляторі область значень хеш-функції ніяк не повинна

перевищувати розмір доступного адресного простору комп'ютера.

Метод організації таблиць ідентифікаторів, заснований на використанні хеш-

адресації, полягає в приміщенні кожного елемента таблиці в комірку, адреса якої

повертає хеш-функція, обчислена для цього елемента. Тоді в ідеальному випадку

для приміщення будь-якого елементу в таблицю ідентифікаторів досить тільки

обчислити його хеш-функцію і звернутися до потрібної осередку масиву даних.

Для пошуку елемента в таблиці також необхідно обчислити хеш-функцію для

шуканого елемента і перевірити, чи не є задана нею осередок масиву порожньою

(якщо вона не порожня - елемент знайдений, якщо порожня - не знайдене).

Спочатку таблиця ідентифікаторів повинна бути заповнена інформацією, яка

дозволила б говорити про те, що все її осередки є порожніми.

Цей метод досить ефективний, оскільки як час розміщення елемента в

таблиці, так і час його пошуку визначаються тільки часом, що витрачається на

обчислення хеш-функції, яке в загальному випадку незрівнянно менше часу,

необхідного для багаторазових порівнянь елементів таблиці.

Метод має два очевидні недоліки. Перший з них - неефективне використання

обсягу пам'яті під таблицю ідентифікаторів: розмір масиву для її зберігання

повинен відповідати всій області значень хеш-функції, в той час як реально

збережених у таблиці ідентифікаторів може бути істотно менше. Другий недолік -

необхідність відповідного розумного вибору хеш-функції. Цей недолік є настільки

істотним, що не дозволяє безпосередньо використовувати хеш-адресацію для

організації таблиць ідентифікаторів.

Проблема вибору хеш-функції не має універсального рішення. Хешування

зазвичай відбувається за рахунок виконання над ланцюжком символів деяких

простих арифметичних і логічних операцій. Найпростішою хеш-функцією для

символу є код внутрішнього уявлення в комп'ютері літери символу. Цю хеш-

функцію можна використовувати і для ланцюжка символів, вибираючи перший

символ в ланцюжку.

Очевидно, що така примітивна хеш-функція буде незадовільною: при її

використанні виникне проблема - двом різним ідентифікаторів, що починається з

однієї і тієї ж букви, буде відповідати одне і те ж значення хеш-функції. Тоді при

хеш-адресації в одну і ту ж комірку таблиці ідентифікаторів повинні бути

поміщені два різних ідентифікатора, що явно неможливо. Така ситуація, коли

двом або більше ідентифікаторів відповідає одне і те ж значення хеш-функції,

називається колізією.

Природно, що хеш-функція, яка припускає колізії, не може бути використана

для хеш-адресації в таблиці ідентифікаторів. Причому досить отримати хоча б

один випадок колізії на всій безлічі ідентифікаторів, щоб такий хеш-функцією не

Page 52: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

52

можна було користуватися. Але чи можливо побудувати хеш-функцію, яка б

повністю виключала виникнення колізій?

Для повного виключення колізій хеш-функція повинна бути взаємно

однозначною: кожному елементу з області визначення хеш-функції має

відповідати одне значення з її безлічі значень, і навпаки - кожному значенню з

безлічі значень цієї функції повинен відповідати тільки один елемент з її області

визначення. Тоді будь-яким двом довільним елементам з області визначення хеш-

функції будуть завжди відповідати два різних її значення. Теоретично для

ідентифікаторів таку хеш-функцію побудувати можна, так як і область визначення

хеш-функції (всі можливі імена ідентифікаторів), і область її значень (цілі

невід'ємні числа) є нескінченними рахунковими множинами, тому можна

організувати взаємно однозначне відображення одної множини на іншу.

Але на практиці існує обмеження, що робить створення взаємно однозначної

хеш-функції для ідентифікаторів неможливим. Справа в тому, що в реальності

область значень будь-хеш-функції обмежена розміром доступного адресного

простору комп'ютера. Безліч адрес будь-якого комп'ютера з традиційною

архітектурою може бути велике, але завжди звичайно, тобто обмежена.

Організувати взаємно однозначне відображення нескінченної кількості на кінцеве

навіть теоретично неможливо. Можна, звичайно, врахувати, що довжина прийому

до уваги частини імені ідентифікатора в реальних компіляторах на практиці також

обмежена - зазвичай вона лежить в межах від 32 до 128 символів (тобто і область

визначення хеш-функції кінцева). Але і тоді кількість елементів в кінцевому

безлічі, що становить область визначення хеш-функції, буде перевищувати їх

кількість в кінцевому безлічі області її значень (кількість всіх можливих

ідентифікаторів більше кількості допустимих адрес в сучасних комп'ютерах).

Таким чином, створити взаємно однозначну хеш-функцію на практиці неможливо.

Отже, неможливо уникнути виникнення колізій.

Тому не можна організувати таблицю ідентифікаторів безпосередньо на

основі однієї лише хеш-адресації. Але існують методи, що дозволяють

використовувати хеш-функції для організації таблиць ідентифікаторів навіть при

наявності колізій.

Хеш-адресація з рехешірованням

Для вирішення проблеми колізії можна використовувати багато способів.

Одним з них є метод рехешірованія (або розстановки). Відповідно до цього

методу, якщо для елемента А адреса n0 = h (A), обчислений за допомогою хеш-

функції h, вказує на вже зайняту комірку, то необхідно обчислити значення

функції n1 = h1 (A) і перевірити зайнятість комірки за адресою п1. Якщо і вона

зайнята, то обчислюється значення h2 (A), і так до тих пір, поки або не буде

знайдена вільна осередок, або чергове значення hi (А) не співпаде з h (A). В

останньому випадку вважається, що таблиця ідентифікаторів заповнена і місця в

ній більше немає - видається інформація про помилку розміщення ідентифікатора

в таблиці.

Тоді пошук елемента А в таблиці ідентифікаторів, організованої таким чином,

буде виконуватися за наступним алгоритмом:

1. Обчислити значення хеш-функції n = h (A) для шуканого елемента А.

Page 53: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

53

2. Якщо осередок за адресою п порожня, то елемент не знайдений, алгоритм

завершений, інакше необхідно порівняти ім'я елемента в комірці n з ім'ям

шуканого елемента A. Якщо вони збігаються, то елемент знайдений і алгоритм

завершений, інакше i: = 1 і перейти до кроку 3.

3. Обчислити ni = hi (A). Якщо осередок за адресою ni порожня або n = ni, то

елемент не знайдений і алгоритм завершений, інакше - порівняти ім'я елемента в

комірці ni з ім'ям шуканого елемента A. Якщо вони збігаються, то елемент

знайдений і алгоритм завершений, інакше i: = i + 1 і повторити крок 3.

Алгоритми розміщення і пошуку елемента схожі по виконуваних операцій.

Тому вони будуть мати однакові оцінки часу, необхідного для їх виконання.

При такій організації таблиць ідентифікаторів у випадку виникнення колізії

алгоритм поміщає елементи в порожні клітинки таблиці, вибираючи їх певним

чином. При цьому елементи можуть потрапляти в осередку з адресами, які потім

будуть збігатися зі значеннями хеш-функції, що призведе до виникнення нових,

додаткових колізій. Таким чином, кількість операцій, необхідних для пошуку або

розміщення в таблиці елемента, залежить від заповнювання таблиці.

Для організації таблиці ідентифікаторів за методом рехешірованія необхідно

визначити всі хеш-функції hi для всіх i. Найчастіше функції hi визначають як деякі

модифікації хеш-функції h. Наприклад, найпростішим методом обчислення

функції hi (A) є її організація у вигляді hi (A) = (h (A) + pi) mod Nm, де pi - деякий

обчислюється ціле число, а Nm - максимальне значення з області значень хеш

функції h. У свою чергу, найпростішим підходом тут буде покласти pi = i. Тоді

отримуємо формулу hi (A) = (h (A) + i) mod Nm. В цьому випадку при збігу

значень хеш-функції для будь-яких елементів пошук вільного осередку в таблиці

починається послідовно від поточної позиції, заданої хеш-функцією h (A).

Цей спосіб не можна визнати особливо вдалим: при збігу хеш-адрес елементи

в таблиці починають групуватися навколо них, що збільшує число необхідних

порівнянь при пошуку і розміщенні. Але навіть такий примітивний метод

рехешірованія є досить ефективним засобом організації таблиць ідентифікаторів

при неповному заповненні таблиці.

Середній час на приміщення одного елемента в таблицю і на пошук елемента

в таблиці можна знизити, якщо застосувати більш досконалий метод

рехешірованія. Одним з таких методів є використання в якості pi для функції hi

(A) = (h (A) + pi) mod Nm послідовності псевдовипадкових цілих чисел p1, p2, ...,

pk. При хорошому виборі генератора псевдовипадкових чисел довжина

послідовності k = Nm.

Існують і інші методи організації функцій рехешірованія hi (A), засновані на

квадратичних обчисленнях або, наприклад, на обчисленні твори за формулою: hi

(A) = (h (A) N · i) mod N'm, де N'm - найближчим просте число, менше Nm. В

цілому рехешірованіе дозволяє досягти непоганих результатів для ефективного

пошуку елемента в таблиці (кращих, ніж бінарний пошук і бінарне дерево), але

ефективність методу сильно залежить від заповнювання таблиці ідентифікаторів

та якості використовуваної хеш-функції - чим рідше виникають колізії, тим вище

ефективність методу. Вимога неповного заповнення таблиці веде до

неефективного використання обсягу пам'яті.

Page 54: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

54

Хеш-адресація з використанням методу ланцюжків

Неповне заповнення таблиці ідентифікаторів при застосуванні рехешірованія

веде до неефективного використання всього обсягу пам'яті, доступного

компілятору. Причому обсяг невикористаної пам'яті буде тим вище, чим більше

інформації зберігається для кожного ідентифікатора. Цього недоліку можна

уникнути, якщо доповнити таблицю ідентифікаторів деякою проміжної хеш-

таблицею.

В комірках хеш-таблиці може зберігатися або пусте значення, яке значення

покажчика на деяку область пам'яті з основної таблиці ідентифікаторів. Тоді хеш-

функція обчислює адресу, за якою відбувається звернення спочатку до хеш-

таблиці, а потім вже через неї по знайденому адресою - до самої таблиці

ідентифікаторів. Якщо відповідна осередок таблиці ідентифікаторів порожня, то

осередок хеш-таблиці буде містити пусте значення. Тоді зовсім не обов'язково

мати в самій таблиці ідентифікаторів осередок для кожного можливого значення

хеш-функції - таблицю можна зробити динамічної, так щоб її обсяг зростав у міру

заповнення (спочатку таблиця ідентифікаторів не містить жодної клітинки, а всі

осередки хеш-таблиці мають пусте значення ).

Такий підхід дозволяє домогтися двох позитивних результатів: по-перше,

немає необхідності заповнювати порожніми значеннями таблицю ідентифікаторів

- це можна зробити тільки для хеш-таблиці; по-друге, кожному ідентифікатору

буде відповідати строго одна осередок в таблиці ідентифікаторів. Порожні

осередки в такому випадку будуть тільки в хеш-таблиці, і обсяг невикористаної

пам'яті не буде залежати від обсягу інформації, що зберігається для кожного

ідентифікатора, - для кожного значення хеш-функції буде витрачатися тільки

пам'ять, необхідна для зберігання одного покажчика на основну таблицю

ідентифікаторів.

На основі цієї схеми можна реалізувати ще один спосіб організації таблиць

ідентифікаторів за допомогою хеш-функції, званий методом ланцюжків. В цьому

випадку в таблицю ідентифікаторів для кожного елемента додається ще одне поле,

в якому може міститися посилання на будь-який елемент таблиці. Спочатку це

поле завжди порожнє (нікуди не вказує). Також необхідно мати одну спеціальну

змінну, яка завжди вказує на першу вільну комірку основної таблиці

ідентифікаторів (спочатку вона вказує на початок таблиці).

Метод ланцюжків працює за наступним алгоритмом:

1. У всі осередки хеш-таблиці помістити порожнє значення, таблиця

ідентифікаторів порожня, змінна FreePtr (покажчик першої вільної комірки) вказує

на початок таблиці ідентифікаторів.

2. Обчислити значення хеш-функції n для нового елемента A. Якщо осередок

хеш-таблиці за адресою n порожня, то помістити в неї значення змінної FreePtr і

перейти до кроку 5; інакше перейти до кроку 3.

3. Вибрати з хеш-таблиці адресу комірки таблиці ідентифікаторів m і перейти

до кроку 4.

4. Для комірки таблиці ідентифікаторів за адресою m перевірити значення

поля посилання. Якщо воно порожнє, то записати в нього адресу з змінної FreePtr і

Page 55: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

55

перейти до кроку 5; інакше вибрати з поля посилання нову адресу m і повторити

крок 4.

5. Додати в таблицю ідентифікаторів новий осередок, записати в неї

інформацію для елемента A (поле посилання повинно бути порожнім), в змінну

FreePtr помістити адреса був призначений додатковий кінцем доданої осередки.

Якщо більше немає ідентифікаторів, які треба помістити в таблицю, то виконання

алгоритму закінчено, інакше перейти до кроку 2.

Пошук елемента в таблиці ідентифікаторів, організованої таким чином, буде

виконуватися за наступним алгоритмом:

1. Обчислити значення хеш-функції n для шуканого елемента A. Якщо

осередок хеш-таблиці за адресою n порожня, то елемент не знайдений і алгоритм

завершений, інакше вибрати з хеш-таблиці адресу комірки таблиці

ідентифікаторів m.

2. Порівняти ім'я елемента в комірці таблиці ідентифікаторів за адресою m з

ім'ям шуканого елемента A. Якщо вони збігаються, то шуканий елемент знайдений

і алгоритм завершений, інакше перейти до кроку 3.

3. Перевірити значення поля посилання в комірці таблиці ідентифікаторів за

адресою m. Якщо воно порожнє, то шуканий елемент не знайдений і алгоритм

завершений; інакше вибрати з поля посилання адреса m і перейти до кроку 2.

При такій організації таблиць ідентифікаторів у випадку виникнення колізії

алгоритм поміщає елементи в осередку таблиці, пов'язуючи їх один з одним

послідовно через поле посилання. При цьому елементи не можуть потрапляти в

осередку з адресами, які потім будуть збігатися зі значеннями хеш-функції. Таким

чином, додаткові колізії не виникають. У підсумку в таблиці виникають своєрідні

ланцюжки зв'язаних елементів, звідки і походить назва даного методу - «метод

ланцюжків».

На рис. 2 проілюстровано заповнення хеш-таблиці і таблиці ідентифікаторів

для ряду ідентифікаторів: A1, A2, A3, A4, A5 за умови, що h (A1) = h (A2) = h (A5)

= n1; h (A3) = n2; h (A4) = n4. Після розміщення в таблиці, щоб знайти код A1 буде

потрібно одне порівняння, для A2 - два порівняння, для A3 - одне порівняння, для

A4 - одне порівняння і для A5 - три порівняння (спробуйте порівняти ці дані з

результатами, отриманими з використанням простого рехешірованія для тих ж

ідентифікаторів).

Метод ланцюжків є дуже ефективним засобом організації таблиць

ідентифікаторів. Середній час на розміщення одного елемента і на пошук елемента

в таблиці для нього залежить тільки від середнього числа колізій, що виникають

при обчисленні хеш-функції. Накладні витрати пам'яті, пов'язані з необхідністю

мати одне додаткове поле покажчика в таблиці ідентифікаторів на кожен її

елемент, можна визнати цілком виправданими, так як виникає економія

використовуваної пам'яті за рахунок проміжної хеш-таблиці. Цей метод дозволяє

більш економно використовувати пам'ять, але вимагає організації роботи з

динамічними масивами даних.

Page 56: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

56

Рисунок. 2. Заповнення таблиці ідентифікаторів при використанні методу

ланцюжків.

Комбіновані способи побудови таблиць ідентифікаторів

Крім рехешірованія і методу ланцюжків можна використовувати комбіновані

методи для організації таблиць ідентифікаторів за допомогою хеш-адресації. В

цьому випадку для виключення колізій хеш-адресація поєднується з одним з

раніше розглянутих методів - простим списком, впорядкованим списком або

бінарним деревом, який використовується як додатковий метод упорядкування

ідентифікаторів, для яких виникають колізії. Причому, оскільки при якісному

виборі хеш-функції кількість колізій зазвичай невелика (одиниці або десятки

випадків), навіть простий список може бути цілком задовільним рішенням при

використанні комбінованого методу.

При такому підході можливі два варіанти: в першому випадку, як і для

методу ланцюжків, в таблиці ідентифікаторів організується спеціальне додаткове

поле посилання. Але на відміну від методу ланцюжків воно має дещо інше

значення: при відсутності колізій для вибірки інформації з таблиці

використовується хеш-функція, поле посилання залишається порожнім.

Якщо ж виникає колізія, то через поле посилання організується пошук

ідентифікаторів, для яких значення хеш-функції збігаються - це поле має

вказувати на структуру даних для додаткового методу: початок списку, перший

елемент динамічного масиву або кореневий елемент дерева.

У другому випадку використовується хеш-таблиця, аналогічна хеш-таблиці

для методу ланцюжків. Якщо за даною адресою хеш-функції ідентифікатор

відсутній, то осередок хеш-таблиці порожня. Коли з'являється ідентифікатор з

даними значенням хеш-функції, то створюється відповідна структура для

додаткового методу, в хеш-таблицю записується посилання на цю структуру, а

ідентифікатор міститься в створену структуру за правилами обраного додатковий

метод.

Page 57: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

57

У першому варіанті при відсутності колізій пошук виконується швидше, але

другий варіант кращий, так як за рахунок використання проміжної хеш-таблиці

забезпечується більш ефективне використання пам'яті.

Як і для методу ланцюжків, для комбінованих методів час розміщення і час

пошуку елемента в таблиці ідентифікаторів залежить тільки від середнього числа

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

використанні проміжної хеш-таблиці мінімальні.

Очевидно, що якщо в якості додаткового методу використовувати простий

список, то вийде алгоритм, повністю аналогічний методу ланцюжків. Якщо ж

використовувати упорядкований список або бінарне дерево, то метод ланцюжків і

комбіновані методи матимуть приблизно рівну ефективність при незначному числі

колізій (поодинокі випадки), але з ростом кількості колізій ефективність

комбінованих методів в порівнянні з методом ланцюжків буде зростати.

Недоліком комбінованих методів є більш складна організація алгоритмів

пошуку і розміщення ідентифікаторів, необхідність роботи з динамічно

розподілюваними областями пам'яті, а також більші витрати часу на розміщення

нового елемента в таблиці ідентифікаторів в порівнянні з методом ланцюжків.

То, який конкретно метод застосовується в компіляторі для організації

таблиць ідентифікаторів, залежить від реалізації компілятора. Один і той же

компілятор може мати навіть кілька різних таблиць ідентифікаторів,

організованих на основі різних методів. Як правило, застосовуються комбіновані

методи.

Створення ефективної хеш-функції - це окреме завдання розробників

компіляторів, і отримані результати, як правило, тримаються в секреті. Гарна хеш-

функція розподіляє надходять на її вхід ідентифікатори рівномірно на всі наявні в

розпорядженні адреси, щоб звести до мінімуму кількість колізій. В даний час існує

безліч хеш-функцій, але, як було показано вище, ідеального хешування досягти

неможливо.

Хеш-адресація - це метод, який застосовується не тільки для організації

таблиць ідентифікаторів у компіляторах. Даний метод знайшов своє застосування і

в операційних системах, і в системах управління базами даних.

Page 58: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

58

Змістовний модуль 4. Багатофайлові проекти. Технологія створення програм. Динамічні структури даних.

Основні розділи теми.

1. Багатофайлові проекти.

2. Директиви препроцесора.

3. Перевантаження функцій.

4. Шаблони функцій.

5. Динамічні структури даних: стек, лінійний список, черга.

6. Бінарні дерева.

7. Призначення лексичного аналізатора

8. Причини, виходячи з яких до складу практично всіх компіляторів

включають лексичний аналіз

9. Проблема визначення меж лексем

10. Таблиця лексем та інформація, що міститься в ній

11. Побудова лексичних аналізаторів (сканерів)

12. Алгоритм роботи найпростішого сканера

13. Призначення синтаксичного аналізатора

14. Проблема розпізнавання ланцюжків КС-мов

15. Види пристроїв розпізнавання для КС-мов

16. Найбільш часто використовувані пристрої розпізнавання

17. Побудова синтаксичного аналізатора

18. Граматики передування

19. Алгоритм «зсув-згортка» для граматик операторного передування

20. Послідовність побудови розпізнавача для КС

Питання для самоконтролю.

1. Складові багатофайлових проектів.

2. Заголовний файл (header file.

3. Ісходний (source file).

4. Два «види глобальності» у багатофайловому проекті.

5. Глобальні змінні.

6. Що і як слід розміщувати в заголовному файлі?

7. Шаблони функцій.

8. Що називається перевантаженням функцій?

9. Який спосіб організації даних називається динамічними структурами даних.

10. Стек.

11. Занесення в стек.

12. Вибірка із стека.

13. Лінійний список.

14. Робота зі списком: додавання, видалення, пошук елементів.

15. Черга.

16. Бінарне дерево.

17. Унікальний ключ.

Page 59: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

59

18. Ліве і праве піддерево.

19. Створення кореневого вузла.

20. Занесення решти вузлів

21. Функція видалення вузла.

22. Призначення синтаксичного аналізатора

23. Призначення лексичного аналізатора

24. Взаємодія лексичного аналізатора та синтаксичного аналізатора

Література: 5 с. 161-165, с. 169-.188. 7 с. 402-404, с. 604-633.

Багатофайлові проекти. Директиви препроцесора.

Перевантаження функцій. Програмування з використанням перевантажених

функцій та шаблонів.

Багатофайлові проекти

Нагадаємо, що користуючись технологією низхідного проектування

програм, ми розбиваємо початкове завдання на підзадачі, потім при необхідності

кожна з них також розбивається на підзадачі, і так далі, поки рішення чергової

підзадачі не виявиться достатньо простим, тобто що реалізовується у вигляді

функції осяжного розміру (як вже вказувалося, найбільш переважним вважається

розмір не більш за одного-двох екрани текстового редактора).

Початкові тексти сукупності функцій для вирішення якої-небудь підзадачі,

як правило, розміщуються в окремому модулі (файлі).

Такий файл називають результатним (source file). Зазвичай він має

розширення .с або .срр. Прототипи всіх функцій початкового файлу виносять в

окремий так званий заголовний файл (header file), для нього прийнято

використовувати розширення .h або .hpp. Таким чином, заголовний файл ххх. h

містить інтерфейс для деякого набору функцій, а результатний файл ххх. срр

містить реалізацію цього набору. Якщо деяка функція з вказаного набору

викликається з якогось іншого початкового модуля ууу. срр, то ви зобов'язані

включити в цей модуль заголовний файл ххх. h за допомогою директиви #include.

Негласне правило стилю програмування на C++ вимагає включення цього ж

заголовного файлу (за допомогою #include) і в початковий файл ххх. срр.

Тепер про глобальні змінні. У багатофайловому проекті можливі два «види

глобальності».

1. Якщо деяка глобальна змінна glvarl оголошена у файлі ххх.срр з

модифікатором static, то вона видима від точки визначення до кінця

цього файлу, тобто область її видимості обмежена файлом.

2. Якщо ж інша глобальна змінна glvar2 оголошена у файлі ххх. срр без

модифікатора statiс, то вона може бути видимою в межах всього проекту.

Правда, для того, щоб вона виявилася видимою в іншому файлі,

необхідно мати в цьому файлі її оголошення з модифікатором extern

(рекомендується це оголошення помістити у файл ххх.h).

Що і як слід розміщувати в заголовному файлі

У заголовному файлі прийнято розміщувати:

визначення типів, що задаються користувачем, констант, шаблонів;

Page 60: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

60

оголошення (прототипи) функцій;

оголошення зовнішніх глобальних змінних (з модифікатором extern);

простори імен.

Тепер звернемо вашу увагу на проблему повторного включення заголовних

файлів. Проблема може виникнути при ієрархічному проектуванні структур даних,

коли в деякий заголовний файл ууу.h включається за допомогою директиви

#include інший заголовний файл ххх.h (наприклад, для використання типів,

визначених в цьому файлі).

Бьерн Страуструп рекомендує використовувати так звану варту

включення, і цей спосіб знайшов широке застосування. Він полягає в

наступному: щоб запобігти повторному включенню заголовних файлів, вміст

кожного .h-файла повинно знаходитися між директивами умовної компіляції

#ifndef і #endif.

Шаблони функцій Області застосування перевантаження функцій і шаблонів відрізняються:

переобтяжені функції ми застосовуємо для оформлення дій, аналогічних по назві, але

різних по реалізації, а шаблони — для ідентичних дій над даними різних типів.

Шаблон функції визначається таким чином:

template <class Туре> тип ім'я ([ список_параметрів ])

{ /* тіло функції */ } Ідентифікатор Туре, задаючий так званий тип, що параметризується, може

використовуватися як в решті частини заголовка, так і в тілі функції. Тип, що

параметризується, — це всього лише фіктивне ім'я, яке компілятор автоматично

замінить ім'ям реального типу даних при створенні конкретної версії функції.

У загальному випадку шаблон функції може містити декілька що

параметризуються типів<class Typel. class Type2, class Type З. ... >.

Процес створення конкретної версії функції називається інстанціюванням

шаблону або створенням екземпляра функції.

Можливі два способи інстанціювання шаблону:

явний, коли оголошується заголовок функції, в якому всі типи, що

параметризуються, замінені на конкретні типи, відомі у цей момент в

програмі

неявний, коли створення екземпляра функції відбувається автоматично,

якщо зустрічається фактичний виклик функції.

Для того, щоб ви прониклися різноманітністю можливостей мови C++, згадаємо,

що шаблони теж можна перенавантажувати, причому як шаблонами, так і звичайними

функціями.

Перевантаженням функцій називається використання декількох функцій з

одним і тим же ім'ям, але з різними списками параметрів.

Переобтяжені функції повинні відрізнятися один від одного або типом хоч

би одного параметра, або кількістю параметрів, або і тим і іншим одночасно.

Перевантаження є видом поліморфізму і застосовується в тих випадках,

коли одне і те ж по сенсу дія реалізується по-різному для різних типів або

Page 61: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

61

структур даних. Компілятор сам визначає, який саме варіант функції викликати,

керуючись списком аргументів.

Якщо ж алгоритм не залежить від типу даних, краще реалізувати його не у

вигляді групи переобтяжених функцій для різних типів, а у вигляді шаблону

функції.

В цьому випадку компілятор сам згенерує текст функції для конкретних

типів даних, з якими виконується виклик, і програмістові не доведеться

підтримувати декілька практично однакових функцій.

Допустимо, вам потрібний проміжний друк різного вигляду: у одному місці

потрібно виводити на екран структуру, в іншому — пару цілих величин з

поясненнями або речовинний масив. щоб в процесі пошуку потрібного варіанту

функції по її виклику не виникало неоднозначності.

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

По-перше, із-за перетворень типів, які компілятор виконує за

замовчуванням.

Правила перетворення арифметичних типів аналогічні. Їх сенс зводиться до

того, що коротші типи перетворяться в довші. Якщо відповідність між

формальними параметрами і аргументами функції на одному і тому ж етапі може

бути одержана більш ніж одним способом, виклик вважається неоднозначним і

видається повідомлення про помилку.

Неоднозначність може також виникнути із-за параметрів за замовчуванням і

посилань.

Правила опису переобтяжених функцій.

Переобтяжені функції повинні знаходитися в одній області видимості, інакше

відбудеться заховання аналогічно однаковим іменам змінних у вкладених

блоках.

Переобтяжені функції можуть мати параметри за замовчуванням, при цьому

значення одного і того ж параметра в різних функціях повинні співпадати. У

різних варіантах переобтяжених функцій може бути різна кількість параметрів

за замовчуванням.

Функції не можуть бути переобтяжені, якщо опис їх параметрів відрізняється

тільки модифікатором const або використанням посилання.

Динамічні структури даних: стек, лінійний список, черга.

Якщо до початку роботи з даними неможливо визначити, скільки пам'яті

буде потрібно для їх зберігання, пам'ять виділяється в міру необхідності окремими

блоками, пов'язаними один з одним за допомогою вказівників. Такий спосіб

організації даних називається динамічними структурами даних, оскільки їх розмір

змінюється під час виконання програми.

З динамічних структур в програмах найчастіше використовуються різні

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

зв'язку окремих елементів і допустимими операціями над ними.

Page 62: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

62

Динамічна структура може займати несуміжні ділянки оперативної пам'яті.

В процесі роботи програми елементи структури можуть в міру необхідності

додаватися і видалятися.

Стек

Стеком називається структура даних, в якій елемент, занесений першим,

витягується останнім.

У алгоритмі швидкого сортування стек використовується для зберігання

меж неврегульованих фрагментів. В принципі, порядок, в якому вони

оброблятимуться, не критичний (головне, щоб врешті-решт всі фрагменти

виявилися відсортованими), але стек використовувати найзручніше із-за простоти

його реалізації.

Для стека визначено всього дві операції: занесення елементу і вибірка

елементу. При вибірці елемент віддаляється із стека, і це якраз те, що нам

потрібно.

Для роботи із стеком достатньо однієї змінної — вказівника на його

вершину. Назвемо її top. Кожен елемент стека повинен містити два цілі числа, що

є ліва і права межі фрагмента масиву, і вказівник на наступний елемент:

Занесення в стек. Щоб занести в стек межі фрагмента, треба передати у функцію push() ці

межі, а також вказівник на вершину стека, в який ми збираємося їх заносити.

Перед межами вказано ключове слово const, щоб підкреслити той факт, що вони

не повинні змінюватися усередині функції.

Вибірка із стека

Stek* pop(Stek* top, int& l, int& r)

{

Stek* pv = top->p; // Спочатку з вершини стека вибирається вказівник на його

наступний елемент, .который стане новою вершиною стека. Цей

вказівник є значенням функції, яке вона повертає

1 = top->left; // Інформаційна частина елементу заноситься в змінні 1 і г, які

передаються в ісходну функцію по посиланню

г = top->right;

delete top;

return pv;

}

Лінійний список

Якщо розмір бази не обмежений, то для її зберігання в оперативній пам'яті

зручно скористатися однозв'язним лінійним списком. Кожен елемент

однозв'язного списку включає інформаційну частину і вказівник на наступний

елемент. Ознакою кінця списку служить нуль в полі вказівника. Список доступний

через вказівник на його перший елемент:

Додавання. Щоб додати елемент списку в базу, треба знати, кого і куди додавати.

Іншими словами, у функцію треба передати вказівник на початок списку і

елемент, що власне додається. Щоб функція могла додавати в список і найперший

елемент, вона повинна повертати вказівник на початок списку:

Page 63: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

63

Man* add(Man* beg, const Man& man);

Видалення.

Щоб видалити елемент із списку, треба заздалегідь знайти цей елемент,

тобто шляхом проглядання списку одержати вказівник на нього.

Пошук.

Пошук повинен виконуватися по заданих критеріях. Режим роботи функції

пошуку — це її внутрішня справа, тому ззовні їй передається тільки вказівник на

початок списку:

Черга

Черга і стек є окремими випадками лінійного списку. Для черги визначено

всього дві операції — переміщення в кінець і вибірка з початку. При вибірці

елемент видаляється з черги (аналогічно стеку і у протилежність списку).

Роботу з чергою виконують три функції. Функція first формує перший

елемент черги і повертає вказівник на нього. Функція add виконує додавання в

кінець, тому їй передається вказівник на кінець черги і елемент, який слід додати,

а повертає вона змінений вказівник на кінець черги. Вибірка виконується з

початку черги, при цьому вказівник на її початок змінюється, тому функція get

одержує цей вказівник через параметри і повертає його нове значення.

Бінарне дерево

Кожен елемент (вузол) дерева характеризується унікальним ключем. Окрім

ключа, вузол повинен містити два посилання: на своє ліве і праве піддерево. У

всіх вузлів лівого піддерева ключі менші, ніж ключ даного елементу, а у всіх

вузлів правого піддерева— більше. У такому дереві, званому деревом пошуку,

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

ліве або праве піддерево залежно від значення ключа в кожному вузлі. Час пошуку

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

його елементів.

Занесення введених відомостей в дерево зручно оформляти у вигляді

функцій. Оскільки процедура створення кореневого вузла відрізняється від

процедур створення решти вузлів, таких функцій буде дві. Для створення

кореневого вузла потрібно передати у функцію структуру Data. Функція повертає

вказівник на створений корінь дерева:

Node * first(Data data);

Занесення решти вузлів сумісно з пошуком, оскільки в обох випадках потрібно

здійснювати спуск по дереву. Для того, щоб її можна було використовувати і при

видаленні вузла з дерева, зручно розширити її інтерфейс, додавши туди, окрім

вказівника на корінь дерева, заносимых/ шуканих даних і режиму роботи, вказівник на

батька вузла і ознаку, до лівого або правого піддерева батька належить шуканий

вузол. Функція повертає вказівник на знайдений/вставлений вузол:

enum Dir {LEFT, RIGHT};

Node* search_insert(Node* root, const Data& data

Action action, Dir& dir, Node*& parent);

Ознака типу Dir передається в визиваючу функцію по посиланню, а вказівник

на батьківський вузол — за адресою, щоб можна було передати назовні їх значення.

Page 64: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

64

Функція видалення одержує як параметр вказівник на вузол, із списку якого слід

видалити запис, і ключ списку.

int remove_fine(Node* p, const Data& data);

Видалити можна будь-який вузол, у тому числі і кореневий, тому функція

remove_node повинна повертати вказівник на корінь дерева на той випадок, якщо він

зміниться.

Видалення вузла відбувається по-різному залежно від його розташування в

дереві. Якщо вузол є листом, тобто не має нащадків, досить обнулити відповідний

вказівник вузла-предка. Якщо вузол має тільки одного нащадка, то цей нащадок

ставиться на місце вузла, що видаляється, а в іншому дерево не змінюється. Найгірше,

коли у вузла є обидва нащадки, але і тут є простій особливий випадок: якщо у його

правого нащадка немає лівого нащадка, вузол, що видаляється, замінюється своїм

правим нащадком, а лівий нащадок вузла, що видаляється, підключається замість

відсутнього лівого нащадка. У загальному випадку на місце вузла, що видаляється,

поміщається найлівіший лист його правого піддерева (або навпаки — найправіший лист

його лівого піддерева). Це не порушує властивостей дерева пошуку.

Корінь дерева видалиться аналогічним чином за винятком того, що

замінюючий його вузол не потрібно під'єднувати до вузла-предка. Натомість

оновлюється вказівник на корінь дерева.

Можна відмітити, що в різних завданнях змінюється тільки інформаційна

частина компонент, а принципи роботи із стеком, чергою або списком

залишаються незмінними, тому фактично функції для роботи з кожною з цих

структур пишуться і відладжуються один раз. Розробники мови теж відмітили

цей факт і включили в стандартну бібліотеку C++ так звані шаблони, що

реалізовують основні динамічні структури даних.

Найбільш важливі моменти цієї теми:

Page 65: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

65

1. Динамічними структурами даних називаються блоки в динамічній пам'яті ,

пов'язані один з одним за допомогою вказівників.

2. Динамічні структури розрізняються способами зв'язку окремих елементів і

допустимими операціями над ними.

3. Елемент будь-якої динамічної структури даних складається з інформаційних полів і

полів вказівників.

4. Найбільш поширеними структурами є лінійний список (однозв'язний або

двозвязковий), стек, черга і бінарне дерево.

5. Для стека визначені операції приміщення елементу у вершину і вибірки елементу з

вершини.

6. Для черги визначені операції приміщення елементу в кінець черги і вибірка

елементу з її початку.

7. Допускається вставляти і видаляти елементи в довільне місце лінійного списку.

8. Бінарне дерево складається з вузлів, кожний з яких містить, окрім даних, не

більше двох посилань на різні бінарні дерева. На кожен вузол є рівно одне

посилання.

9. Кожен вузол дерева характеризується унікальним ключем.

10. Допускається вставляти і видаляти елементи в довільне місце дерева.

11. Якщо для кожного вузла всі ключі його лівого піддерева менше ключа цього

вузла, а всі ключі його правого піддерева — більше, то таке дерево називається

деревом пошуку. У дереві пошуку можна знайти елемент по ключу, рухаючись від

кореня і переходячи на ліве або праве піддерево залежно від значення

ключа в кожному вузлі, що набагато ефективніше за пошук в списку.

12. Динамічні структури в деяких випадках ефективніше реалізовувати за допомогою

масивів .

Проектування лексичного аналізатора.

Призначення лексичного аналізатора Лексичний аналізатор (або сканер) - це частина компілятора, яка читає

літери програми мовою оригіналу і будує з них слова (лексеми) вихідної мови. На

вхід лексичного аналізатора надходить текст вихідної програми, а вихідна

інформація передається для подальшої обробки компілятором на етапі

синтаксичного аналізу і розбору.

Лексема (лексична одиниця мови) - це структурна одиниця мови, яка

складається з елементарних символів мови і не містить в своєму складі інших

структурних одиниць мови. Лексемами мов програмування є ідентифікатори,

константи, ключові слова мови, знаки операцій тощо. Склад можливих лексем

кожної конкретної мови програмування визначається синтаксисом цієї мови.

Page 66: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

66

З теоретичної точки зору лексичний аналізатор не є обов'язковою,

необхідною частиною компілятора. Його функції можуть виконуватися на етапі

синтаксичного аналізу. Однак існує кілька причин, виходячи з яких до складу

практично всіх компіляторів включають лексичний аналіз. Це такі причини:

• спрощується робота з текстом вихідної програми на етапі синтаксичного

розбору і скорочується обсяг оброблюваної інформації, так як лексичний

аналізатор структурує надходячий на вхід вихідний текст програми і видаляє всю

незначущу інформацію;

• для виділення в тексті і розбору лексем можливо застосовувати просту,

ефективну і добре опрацьовану теоретичну техніку аналізу, в той час як на етапі

синтаксичного аналізу конструкцій вихідної мови використовуються досить

складні алгоритми розбору;

• лексичний аналізатор відокремлює складний по конструкції синтаксичний

аналізатор від роботи безпосередньо з текстом вихідної програми, структура якого

може варіюватися в залежності від версії вхідного мови - при такій конструкції

компілятора при переході від однієї версії мови до іншої досить тільки

перебудувати відносно простий лексичний аналізатор.

Функції, які виконуються лексичним аналізатором, і склад лексем, які він

виділяє в тексті вихідної програми, можуть змінюватися в залежності від версії

компілятора. В основному лексичні аналізатори виконують виключення з тексту

вихідної програми коментарів і незначущих прогалин, а також виділення лексем

наступних типів: ідентифікаторів, строкових, символьних і числових констант,

знаків операцій, роздільників і ключових (службових) слів вхідного мови.

У більшості компіляторів лексичний і синтаксичний аналізатори - це

взаємопов'язані частини. Де провести межу між лексичним і синтаксичним

аналізом, які конструкції аналізувати сканером, а які - синтаксичним

розпізнавачем, вирішує розробник компілятора. Як правило, будь-який аналіз

прагнуть виконати на етапі лексичного розбору вхідних програм, якщо він може

бути там виконаний. Можливості лексичного аналізатора обмежені в порівнянні з

синтаксичним аналізатором, так як в його основі лежать більш прості механізми.

Більш докладно про роль лексичного аналізатора в компіляторі і про його

взаємодію з синтаксичним аналізатором можна дізнатися в [1-4, 7].

Проблема визначення меж лексем

У найпростішому випадку фази лексичного і синтаксичного аналізу можуть

виконуватися компілятором послідовно. Але для багатьох мов програмування

інформації на етапі лексичного аналізу може бути недостатньо для однозначного

визначення типу і кордонів чергової лексеми.

Ілюстрацією такого випадку може служити приклад оператора програми на

мові Фортран, коли по частині тексту DO 10 I = 1 ... неможливо визначити тип

оператора (а відповідно, і кордони лексем). У разі DO 10 I = 1.15 це буде

привласнення дійсної змінної DO10I значення константи 1.15 (прогалини в

Фортрані ігноруються), а в разі DO 10 I = 1,15 це цикл з перерахуванням від 1 до

15 з цілочисельної змінної I до мітки 10.

Page 67: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

67

Інша ілюстрація з більш сучасної мови програмування C ++ - оператор

присвоювання k = i +++++ j;, який має тільки одну вірну інтерпретацію (якщо

операції розділити пробілами): k = i ++ + ++ j;.

Якщо неможливо визначити межі лексем, то лексичний аналіз вихідного

тексту повинен виконуватися поетапно. Тоді лексичний і синтаксичний

аналізатори повинні функціонувати паралельно, по черзі звертаючись один до

одного. Лексичний аналізатор, знайшовши чергову лексему, передає її

синтаксичному аналізатору, той намагається виконати аналіз ліченої частини

вихідної програми і може або запросити у лексичного аналізатора наступну

лексему, або зажадати від нього повернутися на кілька кроків назад і спробувати

виділити лексеми з іншими межами. При цьому він може повідомити інформацію

про те, яку лексему слід очікувати. Більш докладно така схема взаємодії

лексичного та синтаксичного аналізаторів описана в [3, 7].

Паралельна робота лексичного і синтаксичного аналізаторів, очевидно,

більш складна в реалізації, ніж їх послідовне виконання. Крім того, такий підхід

вимагає більше обчислювальних ресурсів і в загальному випадку більшого часу на

аналіз вихідної програми, так як допускає повернення назад і повторний аналіз

вже прочитаної частини вихідного коду. Проте складність синтаксису деяких мов

програмування вимагає саме такого підходу - розглянутий раніше приклад

програми на мові Фортран не може бути проаналізований інакше.

Щоб уникнути паралельної роботи лексичного і синтаксичного аналізаторів,

розробники компіляторів і мов програмування часто йдуть на розумні обмеження

синтаксису вхідної мови. Наприклад, для мови C ++ прийнято угоду, що при

виникненні проблем з визначенням кордонів лексеми завжди вибирається лексема

максимально можливої довжини.

У розглянутому вище прикладі для оператора k = i +++++ j; це призведе до

того, що при читанні четвертого знака + з двох варіантів лексем (+ - знак

складання в C ++, а ++ - оператор інкремента) лексичний аналізатор вибере

найдовшу - ++ (оператор інкремента) - і в цілому весь оператор буде розібраний як

k = i ++ ++ + j; (Знаки операцій розділені пробілами), що невірно, тому що

семантика мови C ++ забороняє два оператора інкремента поспіль. Звичайно,

невірний аналіз операторів, аналогічних наведеним у прикладі (бажаючі можуть

переконатися в цьому на кожному доступному компіляторі мови C ++), - незначна

плата за збільшення ефективності роботи компілятора і не обмежує можливості

мови (той же самий оператор може бути записаний у вигляді k = i ++ + ++ j ;, що

виключить будь-які неоднозначності в його аналізі). Однак таким же шляхом для

мови Фортран піти не можна - різниця між оператором присвоювання і

оператором циклу надто велика, щоб нею можна було знехтувати.

Надалі будемо виходити з припущення, що всі лексеми можуть бути

однозначно виділені сканером на етапі лексичного аналізу. Для всіх сучасних мов

програмування це дійсно так, оскільки їх синтаксис розроблявся з урахуванням

можливостей компіляторів.

Таблиця лексем та інформація, що міститься в ній

Page 68: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

68

Результатом роботи лексичного аналізатора є перелік всіх знайдених в тексті

вихідної програми лексем з урахуванням характеристик кожної лексеми. Цей

перелік лексем можна представити у вигляді таблиці, званої таблицею лексем.

Кожній лексемі в таблиці лексем відповідає якийсь унікальний умовний код, що

залежить від типу лексеми, і додаткова службова інформація. Таблиця лексем у

кожному рядку повинна містити інформацію про вид лексеми, її тип і, можливо,

значення. Зазвичай структури даних, службовці для організації такої таблиці,

мають два поля: перше - тип лексеми, друге - покажчик на інформацію про

лексему.

Крім того, інформація про деякі типи лексем, знайдених у вихідній програмі,

повинна поміщатися в таблицю ідентифікаторів (або в одну з таблиць

ідентифікаторів, якщо компілятор передбачає різні таблиці ідентифікаторів для

різних типів лексем).

Увага! Не слід плутати таблицю лексем і таблицю ідентифікаторів - це дві

принципово різні таблиці, оброблювані лексичним аналізатором.

Таблиця лексем фактично містить весь текст вихідної програми, оброблений

лексичним аналізатором. У неї входять всі можливі типи лексем, крім того, будь-

яка лексема може зустрічатися в ній будь-яку кількість разів. Таблиця

ідентифікаторів містить тільки певні типи лексем - ідентифікатори і константи. У

неї не потрапляють такі лексеми, як ключові (службові) слова вхідної мови, знаки

операцій і роздільники. Крім того, кожна лексема (ідентифікатор або константа)

може зустрічатися в таблиці ідентифікаторів тільки один раз. Також можна

відзначити, що лексеми в таблиці лексем обов'язково розташовуються в тому ж

порядку, що і у вихідній програмі (порядок лексем в ній не змінюється), а в

таблиці ідентифікаторів лексеми розташовуються в будь-якому порядку так, щоб

забезпечити зручність пошуку.

Як приклад можна розглянути лише певну частину вихідного коду на мові

Object Pascal і відповідну йому таблицю лексем, представлену в табл. 1:

begin

for i:=1 to N do

fg:= fg * 0.5

Таблиця 1. Лексеми фрагмента програми на мові Pascal

Лексема Тип лексеми Значення

Begin Ключове слово X1

For Ключове слово X2

I Ідентифікатор I : 1

:= Знак присвоєння S1

1 Цілочисельна константа 1

To Ключове слово X3

N Ідентифікатор N : 2

Page 69: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

69

Do Ключове слово X4

Fg Ідентифікатор Fg : 3

:= Знак присвоєння S1

fg Ідентифікатор Fg : 3

* Знак арифметичної операції A1

0.5 Сумісна константа 0.5

Поле «значення» в табл. 1 розуміє певне кодове значення, яке буде

поміщено в підсумкову таблицю лексем в результаті роботи лексичного

аналізатора. Звичайно, значення, які записані в прикладі, є умовними. Конкретні

коди вибираються розробниками при реалізації компілятора. Важливо відзначити

також, що встановлюється зв'язок таблиці лексем з таблицею ідентифікаторів (у

прикладі це відображено деяким індексом, наступним після ідентифікатора за

знаком «:», а в реальному компіляторі визначається його реалізацією).

Побудова лексичних аналізаторів (сканерів)

Лексичний аналізатор має справу з такими об'єктами, як різного роду

константи і ідентифікатори (до останніх відносяться і ключові слова). Мова опису

констант і ідентифікаторів в більшості випадків є регулярною, тобто може бути

описана за допомогою регулярних граматик. Розпізнавачами для регулярних мов є

кінцеві автомати (КА). Існують правила, за допомогою яких для будь-якої

регулярної граматики може бути побудований КА, що розпізнає ланцюжок мови,

заданого цієї граматикою.

Будь-який КА може бути заданий за допомогою п'яти параметрів: M (Q, Σ, δ,

q0, F), де:

Q - кінцева множина станів автомата;

Σ - кінцева множина допустимих вхідних символів (вхідний алфавіт КА);

δ - задане відображення множини Q · Σ в множини підмножин P (Q) δ: Q · Σ

→ P (Q) (іноді δ називають функцією переходів автомата);

- початковий стан автомата;

- множина заключних станів автомата.

Іншим способом опису КА є граф переходів - графічне представлення

множини станів і функції переходів КА. Граф переходів КА - це навантажений

односпрямований граф, в якому вершини представляють стану КА, дуги

відображають переходи з одного стану в інший, а символи навантаження

(позначки) дуг відповідають функції переходу КА. Якщо функція переходу КА

передбачає перехід зі стану до в q 'по кільком символів, то між ними будується

одна дуга, яка позначається усіма символами, за якими відбувається перехід з q в

q'.

Недетермінірованний КА незручний для аналізу ланцюжків, так як в ньому

можуть зустрічатися стани, що допускають неоднозначність, тобто такі, з яких

виходить дві або більше дуги, помічені одним і тим же символом. Очевидно, що

Page 70: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

70

програмування роботи такого КА - нетривіальне завдання. Для простого

програмування функціонування КА M (Q, Σ, δ, q0, F) він повинен бути

детермінованим - в кожному з можливих станів цього КА для будь-якого вхідного

символу функція переходу повинна містити не більше одного стану:

Доведено, що будь-який недетермінірованний КА може бути перетворений в

детермінований КА так, щоб їхні мови збігалися [3, 7, 26] (кажуть, що ці КА

еквівалентні).

Крім перетворення в детермінований КА будь-який КА може бути

мінімізований - для нього може бути побудований еквівалентний йому

детермінований КА з мінімально можливою кількістю станів.

Можна написати функцію, яка відображатиме функціонування будь-якого

детермінованого КА. Щоб запрограмувати таку функцію, досить мати змінну, яка

б відображала поточний стан КА, а переходи з одного стану в інший на основі

символів вхідного ланцюжка можуть бути побудовані за допомогою операторів

вибору. Робота функції повинна тривати до тих пір, поки не буде досягнутий

кінець вхідного ланцюжка. Для обчислення результату функції необхідно по її

завершенні проаналізувати стан КА. Якщо це один з кінцевих станів, то функція

виконана успішно і вхідні ланцюжок приймається, якщо немає, то вхідний

ланцюжок не належить заданій мові.

Однак в загальному випадку задача лексичного аналізатора ширше, ніж

просто перевірка ланцюжка символів лексеми на відповідність її вхідної мови. Він

повинен правильно визначити кінець лексеми (про це було сказано вище) і

виконати ті чи інші дії із запам'ятовування розпізнаної лексеми (занесення її в

таблицю лексем). Набір виконуваних дій визначається реалізацією компілятора.

Зазвичай ці дії виконуються відразу ж при виявленні кінця розпізнається лексеми.

У вхідному тексті лексеми не обмежені спеціальними символами.

Визначення меж лексем - це виділення тих рядків в загальному потоці вхідних

символів, для яких треба виконувати розпізнавання. Якщо кордони лексем завжди

визначаються (а вище було прийнято саме таку угоду), то їх можна визначити по

заданим термінальним символам і по символам початку наступної лексеми.

Термінальні символи - це прогалини, знаки операцій, символи коментарів, а також

роздільники (коми, крапки з комою тощо). Набір таких термінальних символів

може варіюватися в залежності від вхідної мови. Важливо відзначити, що знаки

операцій самі також є лексемами і необхідно не пропустити їх при розпізнаванні

тексту.

Таким чином, алгоритм роботи найпростішого сканера можна описати так:

• проглядається вхідний потік символів програми мовою оригіналу до

виявлення чергового символу, який обмежує лексему;

• для обраної частини вхідного потоку виконується функція розпізнавання

лексеми;

• при успішному розпізнаванні інформація про виділену лексему заноситься

в таблицю лексем, і алгоритм повертається до першого етапу;

• при неуспішному розпізнаванні видається повідомлення про помилку, а

подальші дії залежать від реалізації сканера: або його виконання припиняється,

Page 71: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

71

або робиться спроба розпізнати наступну лексему (йде повернення до першого

етапу алгоритму).

Робота програми-сканера триває до тих пір, поки не будуть переглянуті всі

символи програми мовою оригіналу з вхідного потоку.

Проектування синтаксичного аналізатора.

Мета роботи: вивчення основних понять теорії граматик простого і

операторного передування, ознайомлення з алгоритмами синтаксичного аналізу

(розбору) ДЛЯ деяких класів КС-граматик, створення простого синтаксичного

аналізатора для заданої граматики операторного передування.

Призначення синтаксичного аналізатора

За ієрархією граматик Хамського виділяють чотири основні групи мов (і

граматик, що їх описують). При цьому найбільший інтерес представляють

регулярні і контекстно-вільні (КС) граматики і мови. Вони використовуються при

описі синтаксису мов програмування. За допомогою регулярних граматик можна

описати лексеми мови - ідентифікатори. константи, службові слова та інші. На

основі КС-граматик будуються крупніші синтаксичні конструкції: описи типів і

змінних. арифметичні і логічні вирази, оператори, що управляють, і. нарешті,

повністю вся програма на вхідній мові.

Контекстно-вільна граматика (КС-граматика, Бесконтекстная граматика) -

окремий випадок формальної граматики, у якій ліві частини всіх продукцій є

поодинокими нетерміналами (об'єктами, які позначають будь-яку сутність мови

(наприклад: формула, арифметичний вираз, команда) і не мають конкретного

символьного значення). Сенс терміна «контекстно-вільна» полягає в тому, що є

можливість застосувати продукцію до нетерміналу, причому незалежно від

контексту цього нетермінала (на відміну від загального випадку необмеженої

граматики Хомського).

Мова, яка може бути задана КС-граматикою, називається контекстно-вільним

мовою або КС-мовою.

Вхідні ланцюжки регулярних мов розпізнаються за допомогою кінцевих

автоматів (НО). Вони лежать в основі сканерів, що виконують лексичний аналіз і

виділення слів в тексті програми на вхідній мові. Результатом роботи сканера є

перетворення початкової програми в список або таблицю лексем. Подальшу її

обробку виконує інша частина компілятора - синтаксичний аналізатор. Його

робота заснована на використанні правил КС-граматики, (контекстно свободніе)

що описують конструкції початкової мови.

Синтаксичний аналізатор (синтаксичний пристрій для розбору) - це частина

компілятора. яка відповідає за виявлення і перевірку синтаксичних конструкцій

вхідної мови.

У завдання синтаксичного аналізатора входить:

1. знайти і виділити синтаксичні конструкції в тексті початкової програми;

2. встановити тип і перевірити правильність кожної синтаксичної конструкції;

3. представити синтаксичні конструкції у вигляді, зручному для подальшої

генерації тексту результуючої програми.

Синтаксичний аналізатор - це основна частина компілятора на етапі

аналізу. Без виконання синтаксичного розбору робота компілятора безглузда, тоді

Page 72: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

72

як лексичний розбір, у принципі, не є обов'язковою фазою компіляції. Всі

завдання по перевірці синтаксису вхідної мови можуть бути вирішені на етапі

синтаксичного розбору. Лексичний аналізатор тільки дозволяє позбавити

складний по структурі синтаксичний аналізатор від рішення примітивних

задач по виявленню і запам'ятовуванню лексем початкової програми.

Виходом лексичного аналізатора є таблиця лексем. Ця таблиця утворює вхід

синтаксичного аналізатора, який досліджує тільки один компонент ой лексеми - її

тип. Решта інформації про лексеми використовується на пізніших компіляції при

семантичному аналізі, підготовці до генерації і генерації коду результуючої

програми.

Синтаксичний аналізатор сприймає вихід лексичного аналізатора і розбирає

його відповідно до граматики вхідної мови.

Проте в граматиці вхідної мови програмування звичайно не уточнюється, які

конструкції слід вважати лексемами. Прикладами конструкцій, які звичайно

розпізнаються під час лексичного аналізу, служать ключові слова, константи і

ідентифікатори. Але ці ж конструкції можуть розпізнаватися і синтаксичним

аналізатором. На практиці не існує жорсткого правила, що визначає, які

конструкції повинні розпізнаватися на лексичному рівні, а які треба залишати

синтаксичному аналізатору. Звичайно це визначає розробник компілятора

виходячи з технологічних аспектів програмування, а також синтаксису і

семантики вхідної мови.

У основі синтаксичного аналізатора лежить пристрій для розпізнавання

тексту початкової програми, побудований на основі граматики вхідної мови.

Як правило, синтаксичні конструкції мов програмування можуть бути описані

за допомогою КС-граматик; рідше зустрічаються мови, які можуть бути описані за

допомогою регулярних граматик.

Головну роль в тому, як функціонує синтаксичний аналізатор і який алгоритм

лежить в його основі, грають принципи побудови пристрою для розпізнавання для

КС-мов. Без застосування цих принципів неможливо виконати ефективний

синтаксичний розбір пропозицій вхідної мови.

Проблема розпізнавання ланцюжків КС-мов

Перед синтаксичним аналізатором знаходяться два основні завдання:

перевірити правильність конструкцій програми, яка представляється у вигляді вже

виділених слів вхідної мови, і перетворити її у вигляд, зручний для подальшої

семантичної (смисловий) обробки і генерації коду. Одним із способів такого

представлення є дерево синтаксичного розбору.

Основою для побудови пристрою для розпізнавання КС-мов є автомати з

магазинною пам'яттю - МП-автомати - односторонні недетерміновані пристрої для

розпізнавання з лінійно-обмеженою магазинною пам'яттю. Тому важливо

розглянути, як функціонує МП-автомат і як для КС-мов розв'язується завдання

розбору - побудова пристрою для розпізнавання мови На основі заданої

граматики. Далі розглянуті технічні аспекти, пов'язані з реалізацією синтаксичних

аналізаторів.

Page 73: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

73

МП-автомат на відміну від звичайного НО має стек (магазин). у який можна

поміщати спеціальні – «магазинні» символи (звичайне це термінальні і

нетермінальні символи граматики мови). Перехід МП-автомата з одного стану в

інший залежить не тільки від вхідного символу, але і від одного або декількох

верхніх символів стека. Таким чином, конфігурація автомата визначається

трьома параметрами: станом автомата, поточним символом вхідного

ланцюжка (положенням покажчика в ланцюжку) і вмістом стека.

При виконанні переході МП-автомата з однієї конфігурації в іншу із стека

віддаляються верхні символи, відповідні умові переходу, і додається ланцюжок,

відповідний правилу переходу. Перший символ ланцюжка стає верхівкою стека.

Допускаються переходи, при яких вхідний символ ігнорується (і тим самим він

буде вхідним символом при наступному переході). Ці переходи називаються -

переходами. Якщо при закінченні ланцюжка автомат знаходиться в одному із

заданих кінцевих станів, а стек порожній, ланцюжок вважається прийнятим (після

закінчення ланцюжка можуть бути зроблені -переходи). Інакше ланцюжок

символів не приймається.

МП-автомат називається недетемінованим, якщо при одній і тій же його

конфігурації можливий більш ніж один перехід. Інакше (якщо з будь-якої

конфігурації МП-автомата по будь-якому вхідному символу можливо не більше

одного переходу в наступну конфігурацію) МП-автомат вважається

детермінованим (ДМП-автоматом). ДМП-автомати задають клас детермінованих

КС-мов, для яких існують однозначні КС-граматики. Саме цей клас мов лежить в

основі синтаксичних конструкцій всіх мов програмування, оскільки будь-яка

синтаксична конструкція мови програмування повинна допускати тільки

однозначне трактування.

По довільній КС-граматиці

завжди можна побудувати недетермінований МП-автомат, який допускає

ланцюжки мови, заданої цією граматикою. А на основі цього МП-автомата можна

створити пристрій розпізнавання для заданої мови.

Проте при алгоритмічній реалізації функціонування такого пристрою

розпізнавання можуть виникнути проблеми. Річ у тому, що: побудований МП-

автомат буде, як правило, недетермінованим, а для МП-автоматів, на відміну від

звичайних НО, не існує алгоритму, який дозволяв би перетворити довільний МП-

автомат в ДМП-автомат. Тому програмування функціонування МП-автомата -

нетривіальне завдання. Якщо моделювати його функціонування по кроках з

перебором всіх можливих станів, то може опинитися, що побудований для

тривіального МП-автомата алгоритм ніколи не завершиться на кінцевому

вхідному ланцюжку символів за певних умов.

Тому для побудови пристрою розпізнавання для мови, заданої КС-

граматикою, рекомендується скористатися відповідним математичним апаратом і

одним з існуючих алгоритмів.

Види пристроїв розпізнавання для КС-мов

Page 74: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

74

Існують нескладні перетворення КС-граматик, виконання яких гарантує, що

побудований на основі перетвореної граматики МП-автомат можна буде

промоделювати за кінцевий час на основі кінцевих обчислювальних ресурсів.

Ці перетворення дозволяють будувати два основних типу простих пристроїв

розпізнавання:

пристрій розпізнавання з підбором альтернатив;

пристрій розпізнавання на основі алгоритму «здвиг–згортка».

Роботу пристрою розпізнавання з підбором альтернатив можна неформально

описати таким чином: якщо на верхівці стека МП-автомата знаходиться

нетермінальний символ А, то його можна замінити на ланцюжок символів о. за

умови, що в граматиці мови є правило А→α не зрушуючи при цьому читаючу

головку автомата (цей крок роботи називається «підбір альтернативи»); якщо ж на

верхівці стека знаходиться термінальний символ би, який співпадає з поточним

символом вхідного ланцюжка, то цей символ можна викинути із стека і

пересунути ту, що прочитує головку на одну позицію управо (цей крок роботи

називається викид). Даний МП-автомат може бути недетермінованим. оскільки

при підборі альтернативи в граматиці мови може опинитися більше одного

правила виду А→α, тоді функція δ( q,λ,А) міститиме більше одного наступного

стану - у МП-автомата буде декілька альтернатив.

Рішення про те, чи виконувати на кожному кроці роботи МП-автомата викид

або підбір альтернативи, ухвалюється однозначно. Моделюючий алгоритм

повинен забезпечувати вибір однієї з можливих альтернатив і зберігання

інформації про те, які альтернативи на якому кроці вже були вибрані. щоб мати

можливість повернутися до цього кроку і підібрати інші альтернативи.

Пристрій розпізнавання з підбором альтернатив є низхідним пристроєм

розпізнавання: він читає вхідний ланцюжок символів зліва направо і будує

лівобічний висновок. Назва «низхідний» дано йому тому. що дерево виведення в

цьому випадку слід будувати зверху вниз, від кореня до кінцевих вершин –

«листя» (на відміну від звичайних дерев, корінь у синтаксичного дерева виведення

знаходиться вгорі, а листя - знизу).

Роботу пристрою розпізнавання на основі алгоритму «здвиг-згортка». можна

описати так: якщо на верхівці стека МП-автомата знаходиться ланцюжок символів

У. то її можна замінити на нетермінальний символ А за умови, що в граматиці

мови існує правило вигляду А У. не зрушуючи при цьому читаючу головку

автомата (цей крок роботи називається «згортка»); з іншого боку, якщо читаюча

головка автомата оглядає деякий символ вхідного ланцюжка а, то його можна

помістити в стек, зрушивши при цьому головку на одну позицію управо (цей крок

роботи називається «здвиг» або «перенесення»).

Цей пристрій розпізнавання потенційно має більше за неоднозначностей чим

розглянутий вище за пристрій розпізнавання, заснований на алгоритмі підбору

альтернатив. На кожному кроці роботи автомата треба вирішувати наступні

питання:

що необхідно виконувати: здвиг або згортку;

Page 75: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

75

якщо виконувати згортку, то такий ланцюжок у вибрати для пошуку

правил (ланцюжок у повинна зустрічатися в правій частині правил

граматики);

яке правило вибрати для згортки, якщо опиниться, що існує декілька

правил вигляду Ару (дещо правил з однаковою правою частиною).

Для моделювання роботи цього розширеного МП-автомата треба на кожному

кроці запам'ятовувати всі зроблені дії, щоб мати можливість повернутися до вже

зробленого кроку і виконати ці ж дії по-іншому. Цей процес повинен

повторюватися до тих пір, поки не будуть перебрані всі можливі варіанти.

Пристрій розпізнавання на основі алгоритму «згортка зрушення» є висхідним

пристроєм розпізнавання: він читає вхідний ланцюжок символів зліва направо і

будує правосторонній висновок. Назва висхідний дано йому тому, що дерево

висновку в цьому випадку слід будувати від низу до верху, від кінцевих вершин до

кореня.

Функціонування обох розглянутих пристроїв розпізнавання реалізується

достатньо простими алгоритмами. Проте обидва вони мають один істотний

недолік - час їх функціонування експоненціально залежить від довжини вхідного

ланцюжка n=|α|, що неприпустимо для компіляторів, де довжина вхідних програм

складає від десятків до сотень тисяч символів. Так відбувається тому, що обидва

алгоритми виконують розбір вхідного ланцюжка символів методом простого

перебору, підбираючи правила граматики довільним чином, а у разі невдачі

повертаються до вже прочитаної частини вхідного ланцюжка і намагаються

підібрати інші правила.

Існують ефективніші табличні пристрої розпізнавання, побудовані на основі

алгоритмів Эрлі і Кока-Янгера-Касамі. Вони забезпечують поліноміальну

залежність часу функціонування від довжини вхідного ланцюжка (n3 для

довільного МП-автомата і n2 для ДМП-автомата), Це найефективніші з

універсальних пристроїв розпізнавання для КС-мов. Але і поліноміальну

залежність часу розбору від довжини вхідного ланцюжка не можна визнати

задовільною.

Кращих універсальних пристроїв розпізнавання не існує. Проте серед всього

типу КС-мов існує безліч класів і підкласів мов, для яких можна побудувати

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

довжини вхідного ланцюжка символів. Такі пристрої розпізнавання називають

лінійними пристроями розпізнавання КС-мов.

В теперішній час відомо безліч лінійних пристроїв розпізнавання і

відповідних їм класів КС-мов. Кожний з них має свій алгоритм функціонування,

але всі відомі алгоритми є модифікацією двох базових алгоритмів - алгоритму з

підбором альтернатив і алгоритму згортка зрушення, розглянутих вище.

Модифікації полягають в тому, що алгоритми виконують підбір правил граматики

для розбору вхідного ланцюжка символів не довільним чином, а керуючись

встановленим порядком, який створюється заЀ раніше на основі заданої KC-

граматика. Такий підхід дозволяє уникнути повернень до вже прочитаної частини

ланцюжка і істотно скорочує час, потрібний на її розбір.

Page 76: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

76

Серед всієї множини можна виділити наступні найбільш часто

використовувані пристрої розпізнавання:

пристрої розпізнавання на основі рекурсивного спуску (модифікація

алгоритму з підбором альтернатив);

пристрої розпізнавання на основі LL(1) - і LL(k) -граматик

(модифікація алгоритму з підбором альтернатив);

пристрої розпізнавання на основі LR(0) - і LR(1) -граматик

(модифікація алгоритму згортка зрушення);

пристрої розпізнавання на основі SLR( 1) - і LALR(1 ) -граматик

(модифікація алгоритму згортка зрушення );

пристрої розпізнавання на основі граматик передування (модифікація

алгоритму згортка зрушення).

Побудова синтаксичного аналізатора

Синтаксичний аналізатор повинен розпізнавати весь текст початкової

програми. Тому, на відміну від лексичного аналізатора, йому немає необхідності

шукати межі розпізнаваного рядка символів. Він повинен сприймати всю

інформацію. що поступає йому на вхід, і або підтвердити її приналежність

вхідному мова або повідомити про помилку в початковій програмі.

Але, як і у разі лексичного аналізу, завдання синтаксичного аналізу не

обмежується тільки перевіркою приналежності ланцюжка заданій мові. Необхідно

оформити знайдені синтаксичні конструкції для подальшої генерації тексту

результуючою програмою. Синтаксичний аналізатор повинен мати якусь вихідну

мову, за допомогою якої він передає наступним фазам компіляції інформацію про

знайдені і розібрані синтаксичні структури. У такому разі він вже є не різновидом

МП-автомату, а перетворювачем з магазинною пам'яттю - МП-перетворювачем.

Побудова синтаксичного аналізатора - це більш творчий процес, ніж

побудова лексичного аналізатора. Цей процес не завжди може бути повністю

формалізований.

Маючи граматику вхідної мови, розробник синтаксичного аналізатора

повинен в першу чергу виконати ряд формальних перетворень над цією

граматикою, що полегшують побудову пристрою розпізнавання. Після цього він

повинен перевірити, чи відноситься одержана граматика до одного з відомих

класів КС-мов, для яких існують лінійні пристрої розпізнавання. Якщо такий клас

знайдений, можна будувати пристрій розпізнавання (якщо знайдено декілька

класів, слід вибрати той, для якого побудова пристрою розпізнавання простіше

або побудований пристрій розпізнавання володітиме кращими характеристиками).

Якщо ж такий клас КС-мов знайти не вдалося, то розробник повинен спробувати

виконати над граматикою деякі перетворення, щоб привести її до одного з відомих

класів. Ці перетворення не можуть бути описані формально, і у кожному

конкретному випадку розробник повинен спробувати знайти їх сам (іноді

перетворення має сенс шукати навіть у тому випадку, коли граматика підпадає під

один з відомих класів КС-мов, з метою знайти інший клас, для якого можна

побудувати кращий по характеристиках пристрій розпізнавання).

Page 77: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

77

Складнощів з побудовою синтаксичних аналізаторів не існувало б, якби для

KC-граматик були дозволені проблеми перетворення і еквівалентності. Але

оскільки в загальному випадку це не так, то одним класом КС-граматик, для якого

існують лінійні пристрої розпізнавання, обмежитися не вдається. З цієї причини

для всіх класів КС-граматик існує принципово важливе обмеження: У загальному

випадку неможливо перетворити довільну КС-граматику до вигляду, потрібного

даним класом КС-граматик, або ж довести, що такого перетворення не існує. Те,

що проблема нерозв'язна в загальному випадку, не говорить про те, що вона не

розв'язується в кожному конкретному окремому випадку, і часто вдається знайти

такі перетворення. І чим ширше набір класів KC-граматик з лінійними пристроями

розпізнавання, тим простіше за них шукати.

Тільки, коли в результаті всіх цих дій не вдалося знайти відповідний клас КС-

мов, розробник вимушений будувати універсальний пристрій розпізнавання.

Характеристики такого пристрою розпізнавання будуть істотно гірші. чим у

лінійного пристрою розпізнавання: в кращому разі вдається досягти квадратичної

залежності часу роботи пристрою розпізнавання від довжини вхідного ланцюжка.

Таке рідкісне, тому всі сучасні компілятори побудовані на основі лінійних

пристроїв розпізнавання (інакше час їх роботи був би неприпустимо великий).

Часто одна і та ж КС-граматика може бути віднесена не до одного. а відразу

до декількох класів КС-граматик, що допускають побудову лінійних

распознавателей. Тоді необхідно вирішити, який з декількох можливих пристроїв

розпізнавання вибрати для практичної реалізації.

Відповісти на це питання не завжди легко, оскільки можуть бути побудовані

два принципово різних пристрою розпізнавання, алгоритми роботу яких неможна

співставити. В першу чергу йдеться саме про висхідних і низхідних пристроїв

розпізнавання: у основі перших лежить алгоритм підбору альтернатив, в основі

других - алгоритм згортка зрушення.

На питання про те, якій пристрій розпізнавання - низхідний або висхідний -

вибрати для побудови синтаксичного аналізатора, немає однозначної відповіді.

Цю проблему необхідно вирішувати, спираючись на якусь додаткову інформацію.

ПОРАДА

Слід пригадати, що синтаксичний аналізатор - це один з етапів компіляції. І з

цієї точки зору результати роботи пристрою розпізнавання служать початковими

даними для наступних етапів компіляції. Тому вибір того або іншого пристрою

розпізнавання багато в чому залежить від реалізації компілятора, від того, які

принципи покладені в його основу.

Бажання використовувати простіший клас граматик для побудови пристрою

розпізнавання може зажадати якихось маніпуляцій із заданою граматикою,

необхідних для її перетворення до необхідного класу. При цьому нерідко

граматика стає неприродною і малозрозумілою, що надалі утрудняє її

використання для генерації результуючого коду. Тому буває зручним

використовувати початкову граматику такої, яка вона є, не прагнучи перетворити

її до простішого класу

В цілому слід зазначити, що, з урахуванням всього сказаного. інтерес

представляють як лівобічний, так і правосторонній аналіз, Конкретний вибір

Page 78: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

78

залежить від реалізації конкретного компілятора, а також від складності граматики

вхідної мови програмування.

У загальному вигляді процес побудови синтаксичного аналізатора можна

описати таким чином:

1. Виконати прості перетворення над заданою KC-граматикою.

2. Перевірити приналежність KC-граматики, що вийшла в результаті

перетворень, до одного з відомих класів КС-граматик, для яких існують

лінійні пристрої розпізнавання.

3. Якщо відповідний клас знайдений, узяти за основу для побудови

пристрою розпізнавання алгоритм розбору вхідних ланцюжків, відомий для

цього класу, якщо знайдено декілька класів лінійних пристроїв розпізнавання

- вибрати з них один на свій розсуд.

4. Інакше, якщо відповідний клас по п. 2 не був знайдений чи ж

знайдений клас КС-граматик не влаштовує розробників компілятора -

спробувати виконати над граматикою неформальні перетворення з метою

підвести її під клас КС-граматик, що цікавить, для лінійних пристроїв

розпізнавання і повернутися до п. 2.

5. Якщо ж ні в п. 3, ні в п. 4 відповідний пристрій розпізнавання знайти

не вдалося (що для сучасних мов програмування практично неможливо).

необхідно використовувати один з універсальних пристроїв розпізнавання.

6. Визначити, в якій формі синтаксичний пристрій розпізнавання

передаватиме результати своєї роботи іншим фазам компілятора (ця форма

називається внутрішнім представленням програми в компіляторі).

Реалізувати вибраний в п. 3 або 5 алгоритм з урахуванням структур даних,

відповідних п. 6.

Граматики передування

КС- мови діляться на класи відповідно до структури правил їх граматик. В

кожному з класів накладаються додаткові обмеження на допустимі правила

граматики. Одним з таких класів є клас граматик передування. Вони

використовуються для синтаксичного розбору ланцюжків за допомогою

модифікацій алгоритму «зсув-згортка».

Принцип організації розпізнавача на основі граматики передування виходить

з того, що для кожної впорядкованої пари символів в граматиці встановлюється

відношення, зване відношенням передування. В процесі розбору МП - автомат

порівнює поточний символ вхідного ланцюжка з одним з символів, що

знаходяться на верхівці стека автомата. В процесі порівняння перевіряється, яке з

можливих відносин передування існує між цими двома символами. Залежно від

знайденого відношення виконується або зрушення, або згортка За відсутності

відношення передування між символами алгоритм сигналізує про помилку.

Завдання полягає в тому. щоб мати можливість несуперечливим чином

визначити відносини передування між символами граматики. Якщо це можливо,

то граматика може бути віднесена до одного з класів граматик передування.

Відношення передування позначатимемо знаками «-.», «<.» і «.>».

Відношення передування єдине для кожної впорядкованої пари символів. При

цьому між якими-небудь двома символами може і не бути відношення

Page 79: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

79

передування це означає, що вони не можуть знаходитися поряд або в одному

елементі розбору синтаксично правильного ланцюжка. Відношення передування

залежать від порядку, і якому коштують символи, і в цьому сенсі їх не можна

плутати із знаками математичних операцій (хоча але зовнішньому вигляду вони

дуже схожі) - вони не володіють ні властивістю комутативності, ні властивістю

асоціативності. Наприклад, якщо відомо, що В1 .> В, то не обов'язково

виконується В <. В1 тому знаки передування позначають спеціальною крапкою:

«=.» «<.» «.>»). Метод передування заснований на тому факті, що відносини

передування між двома сусідніми символами розпізнаваного рядка відповідають

трьом наступним варіантам:

Вi <. В i+1 якщо символ В i+1 - крайній лівий символ деякої основи (це

відношення між символами можна назвати «передує основі» або просто

«передує»);

Вi. > В i+1, якщо символ Вi — крайній правий символ деякої основи (це

відношення між символами можна назвати «слідує за основою» або просто

«слідує»);

Вi =. В i+1 якщо символи В i і В i+1 належать одній основі (це

відношення між символами можна назвати «складають основу»).

Виходячи з цих співвідношень виконується розбір вхідного рядка для

граматик передування.

Суть принципу такого розбору пояснює рис. 1. На ньому зображений вхідний

ланцюжок символів α β γ δ в той момент, коли виконується згортка ланцюжка γ.

Символ α є останнім символом підланцюжка α , а символ β— першим символом

під ланцюжка β. Тоді, якщо в граматиці вдасться встановити несуперечливі

відносини передування, то в процесі виконання розбору по алгоритму «зсув-

згортка» можна завжди виконувати зрушення до тих пір, поки між символом на

верхівці стека і поточним символом вхідного ланцюжка існує відношення <. або =.

А як тільки між цими символами буде виявлено відношення .>, відразу треба

виконувати згортку. Причому для виконання згортки із стека треба вибирати всі

символи, зв'язані відношенням =. Всі різні правила в граматиці передування

повинні мати різні праві частини — це гарантує несуперечність вибору правила

при виконанні згортки.

Рисунок 1. Відношення між символами вхідного ланцюжка в граматиці

передування

Таким чином, встановлення несуперечливих відношень передування між

символами граматики в комплексі з неспівпадаючими правими частинами різних

правил дає відповіді на всі питання, які треба вирішити для організації роботи

алгоритму «зсув-згортка» без повернень.

На підставі відносин передування будують матрицю передування граматики.

Рядки матриці передування позначаються першими (лівими) символами, стовпці -

Page 80: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

80

другими (правими) символами відносин передування. У клітки матриці на

перетині відповідного стовпця і рядка поміщаються знаки відносин. При цьому

порожні клітки матриці говорять про те. що між даними символами немає

жодного відношення передування. Існує декілька видів граматик передування.

Вони розрізняються по тому, які відносини передування в них визначені і між

якими типами символів (термінальними або нетермінальними) можуть бути

встановлені ці відносини. Крім того, можливі незначні модифікації

функціонування самого алгоритму «зсув-згортка» в розпізнавачах для таких

граматик (в основному на етапі вибору правила для виконання згортки, коли

можливі неоднозначності).

Виділяють наступні види граматик передування:

простого передування;

розширеного передування;

слабкого передування;

змішаній стратегії передування;

операторного передування.

Матрицю операторного передування КС- граматики можна побудувати,

спираючись безпосередньо на визначення відношень передування, але простіше і

зручніше скористатися двома додатковими типами множин — множиною крайніх

лівих і крайніх правих символів, а також множиною крайніх лівих термінальних і

крайніх правих термінальних.

Якщо є КС- граматика G( VT,VN,P,S), V = VT U VN. то множина крайніх

лівих і крайніх правих символів визначається таким чином:

L(U)={T|Э U =>*Tz}множина крайніх лівих символів щодо

нетермінального символу U;

R(U)={T|Э U =>* zT} множина крайніх правих символів щодо

нетермінального символу U.

де U — заданий нетермінальний символ (U є VN), T — будь-який символ

граматики (T є V).a z — довільний ланцюжок символів(z є V*). Ланцюжок z може

бути і порожнім ланцюжком).

Множина крайніх лівих і крайніх правих термінальних символів визначається

таким чином:

—множина крайніх лівих термінальних символів

щодо нетермінального символу U;

—множина крайніх правих термінальних символів

щодо нетермінального символу U

де t— термінальний символ ( ),U i C — нетермінальні символи ( ), аz

довільний ланцюжок символів( ланцюжокz може бути і пустим ланцюжком).

Множини L(U) і R(U) можуть бути побудовані для кожного нетермінального

символу за прoстим алгоритмом:

1. Для кожного нетермінального символу U шукаємо всі правила, що

містять U в лівій частині. В множині L(U) включаємо самий лівий символ з

правої частини правила, а в множині R(U) – самий правий символ з правої

частини (тобто в множині L (U) записуємо всі символи, з яких починаються

правила для символу U, а в множині R(U) — символи, якими ці правила

Page 81: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

81

закінчуються). Якщо в правій частині правила для символу U є тільки один

символ, то він повинен бути записаний в обидві множини L(U) і R(U).

2. Для кожного нетермінального символу U виконуємо наступне

перетворення: якщо множина L(U) містить нетермінальні символи граматики

U’ U".... то його потрібно доповнити символами, що входять у відповідну

множину і що не входять в , Ту ж операцію потрібно виконати

для R(U). Фактично, якщо якийсь символ U входить в одну з множин для

символу U, то потрібно об'єднати множини для U" та U, а результат записати

в множину для символу U.

3. Якщо на попередньому кроці хоч би одна множина L(U)або R(U) для

деякого символу граматики змінилося, то треба повернутися до кроку 2, інакше –

побудова закінчена.

Для знаходження множини використовується наступний алгоритм:

1. Для кожного нетермінального символу граматики U будуються

множини L(U) та R(U).

2. Для кожного нетермінального символу граматики U шукаються

правила виду ;термінальні символиt включаються в

множину . Аналогічно для множини шукаються правила

вигляду (тобто в множину записуються всі крайні зліва

термінальні символи з правих частин правил для символу U, а в множину

— всі крайні справа термінальні символи цих правил). Не виключено, що

один і той же термінальний символ буде записаний в обидві множини .

3. Проглядається множина L(U), в яку входять символи .

Множина доповнюється термінальними символами, що входять в .. і

що не входять в . Аналогічна операція виконується і для множини на

основі множини R (U).

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

термінальними символами (початок і кінець ланцюжка). Для них

визначені наступні відносини передування:

Маючи побудовану множину , заповнення матриці операторного

передування для КС- граматики G(VT.VN,P,S) можна виконати по наступному

алгоритму:

1. Беремо перший символ з множини термінальних символів граматики

VT: VT. і=1. Вважатимемо цей символ поточним термінальним символом.

2. У всій множині правил Р шукаємо правила де

поточний термінальний символ, — довільний термінальний символ ,U i

C —довільні нетермінальні символи( ). а х та у — довільні ланцюжки

символів, можливо порожні ( ). Фактично проводиться пошук таких

правил, в яких в правій частині символи а, іb, стоять поряд або ж між ними, та

є не більш за один нетермінальний символ (причому символ обов'язково

стоїть зліва від .

Page 82: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

82

3. Для всіх символів знайдених на кроці 2, виконуємо наступне:

ставимо знак «=» («основа») у клітинки матриці операторного передування на

перетині рядка, поміченого символом і стовпця, поміченого символом

4. У всій множині правил Р шукаємо правила вигляду де а, —

поточний термінальний символ, i С— довільні нетермінальні символи ( ),а

х тау- довільні ланцюжки символів, можливо порожні( ). Фактично

шукаємо правила, в яких в правій частині символ стоїть зліва від

нетермінального символуUj/

4. Для всіх символів U знайдених на кроці 4, беремо множину символів

Lt(Uj). Для всіх термінальних символів ААА що входять в цю множину,

виконуємо наступне: ставимо знак «<.» («передує») в клітинки матриці

операторного передування на перетині рядка, поміченого символом ААА і

стовпця, поміченого символом АА.

5. У всій множині правил Р шукаємо правила виду де —поточний

термінальний символ, UJ i C - довільні нетермінальні символи ( ), а х та

у— довільні ланцюжки символів, можливо порожні ( ). Фактично

шукаємо правила, в яких правої частини символу а о стоїть праворуч від

нетермінального символуUJ .

6. Для всіх символів UJ знайдених на кроці 6 беремо множину символів

Rt(UJ). Для всіх термінальних символів ck що входять в цю множину,

виконуємо наступне: ставимо знак «.>» («слідує») в клітинці матриці

операторного передування на перетині рядка, поміченого символом cк, і

стовпця, поміченого символом aі.

7. Якщо розглянуті всі термінальні символи з множини VT, то

переходимо до кроку 9, інакше - беремо черговий символ з множини VT,

і=і+1, робимо його поточним термінальним символом і повертаємося до

кроку 2.

8. Беремо множину Lt(S) для цільового символу граматики S. Для всіх

термінальних символів cк що входять в цю множину, виконуємо наступне:

ставимо знак «<.» («передує») в клітинці матриці операторного передування

на перетині рядка, поміченого символом («початок рядку»), та стовпця,

поміченого символом cк.

10. Беремо множину R(S) для цільового символу граматики S. Для всіх

термінальних символів cк що входять в цю множину, виконуємо наступне:

ставимо знак «.>» («слідує») в клітинці матриці операторного передування на

перетині рядка, поміченого символом і стовпця, поміченого символом кінця рядка.

Побудова матриці закінчена.

Якщо на всіх кроках алгоритму побудови матриці операторного передування

не виникло суперечностей, коли в одну і ту ж клітинку матриці треба записати два

або три різні символи передування, то матриця побудована правильно (у кожній

клітці такої матриці присутній одні з символів передування — «.>, «<.» або «=» —

або ж клітка порожня). Якщо на якомусь кроці виникла суперечність, значить,

початкова КС- граматика не є граматикою операторного передування. В цьому

випадку можна спробувати перетворити граматику так, що вона стане

Page 83: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

83

задовольняти вимогам операторного передування (що не завжди можливо), або

необхідно використовувати інший тип розпізнавача.

Алгоритм «зсув-згортка» для граматик операторного передування

Алгоритм «зсув-згортка» для граматики операторного передування

виконується МП- автоматом з одним станом. Для моделювання його роботи

необхідний вхідний ланцюжок символів та стек символів, в якому автомат може

звертатися не тільки до самого верхнього символу, але і до деякого ланцюжка

символів на вершині стека.

Цей алгоритм для заданої КС- граматики за наявності побудованої

матриці передування можна описати таким чином:

1. Помістити у верхівку стека символ початку рядка, зчитуючу голівку

МП- автомата помістити в початок вхідного ланцюжка (поточним вхідним

символом стає перший символ вхідного ланцюжка). У кінець вхідного

ланцюжка треба дописати символ кінця рядка.

2. У стеку шукається самий верхній термінальний символ (якщо на

вершині стека лежать нетермінальні символи, вони ігноруються і береться

перший термінальний символ, що знаходиться під ними), при цьому сам

символ s залишається в стеку. З одного ланцюжка береться поточний символ

(справа зчитуючою головки МП- автомата).

3. Якщо символ — це символ початку рядка, а символ — символ кінця

рядка, то алгоритм завершений, вхідний ланцюжок символів розібраний.

4. У матриці передування шукається клітинка на перетині рядка,

поміченого символом і стовпця, поміченого символом виконується

порівняння поточного вхідного символу і термінального символу на верхівці

стека).

5. Якщо клітинка, знайдена на кроці 3, порожня, то значить, вхідний

рядок символів не приймається МП- автоматом, алгоритм переривається і

видасть повідомлення про помилку.

6. Якщо клітинка, знайдена з кроці 3, містить символ «=.» («складає

основу») або «<.» («передує»), то необхідно виконати перенесення

(зрушення). При виконанні перенесення поточний вхідний символ

поміщається на верхівку стека, зчитуюча головка МП- автомату у вхідному

ланцюжку символів зрушується на одну позицію вправо (після чого поточним

вхідним символом стає наступний символ ai+1, i:=i+1). Після цього треба

повернутися до кроку 2.

7. Якщо клітинка, знайдена на кроці 3, містить символ «=.» («слідує»), то

необхідно провести згортку. Для виконання згортки із стека вибираються всі

термінальні символи, зв'язані відношенням «.>»(«складає основу»),

починаючи від вершини стека, а також всі нетермінальні символи, лежачі в

стеку поряд з ними. Ці символи виймаються із стека і збираються в ланцюжок

(якщо в стеку немає символів, зв'язаних відношенням «=», то з нього

виймається один самий верхній термінальний символ та лежачі поряд з ним

нетермінальні символи).

8. У всій множині правил Р граматики G(VT.VN,P,S) шукається правило,

у якого права частина співпадає з ланцюжком (за умовами граматик

Page 84: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

84

передування всі праві частини правил повинні бути різні, тому може бути

знайдено або одне таке правило, або жодного). Якщо правило знайдене. то в

стек надходить нетермінальний символ з лівої частини правила, інакше, якщо

правило не знайдене, це означає, що вхідний рядок символів не приймається

МП- автоматом, алгоритм переривається та видасть повідомлення про

помилку. Слід зазначити, що при виконанні згортки зчитувана головка

автомата не зрушується і поточний вхідний символ залишається незмінним.

Після виконання згортки необхідно повернутися до кроку 2.

Після завершення алгоритму рішення про ухвалення ланцюжка залежить від

вмісту стека. Автомат приймає ланцюжок, якщо в результаті завершення

алгоритму він знаходиться в стані, коли в стеку знаходяться початковий символ

граматики S . Виконання алгоритму може бути перерване, якщо на одному з його

кроків виникне помилка. Тоді вхідний ланцюжок не приймається.

Алгоритм «зсув-згортка» для граматики операторного передування ігнорує

нетермінальні символи. Тому має сенс перетворити початкову граматику так, щоб

залишити в ній тільки один нетермінальний символ. Це перетворення полягає в

тому, що всі нетермінальні символи в правилах граматики замінюються на один

нетермінальний символ (найчастіше — цільовий символ граматики).

Побудована в результаті такого перетворення граматика називається

остовною граматикою, а саме перетворення - остовним перетворенням.

Остовне перетворення не веде до створення еквівалентної граматики і

виконується тільки для спрощення роботи алгоритму (який при виборі правил все

одно ігнорує нетермінальні символи) після побудови матриці передування.

Одержана в результаті остовного перетворення граматика може не бути

однозначною, але всі необхідні дані про порядок застосування правил містяться в

матриці передування і розпізнавач залишається детермінованим. Тому остовне

перетворення може виконуватися без втрат інформації тільки після побудови

матриці передування. При цьому також необхідно стежити, щоб в граматиці не

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

з'явитися в остовній граматиці. Висновок, одержаний при розкладі на основі

остовної граматики, називають результатам остовного розкладу, або остовним

висновком. За наслідками остовного розкладу можна побудувати відповідний

йому вивід на основі правил початкової граматики. Проте це завдання не

представляє практичного інтересу, оскільки остовний висновок відрізняється від

висновку на основі початкової граматики тільки тим, що в нім відсутні кроки,

пов'язані з застосуванням кільцевих правил, і не враховуються типи

нетермінальних символів. Для компіляторів розпізнавання ланцюжків вхідної

мови полягає не в знаходженні того чи іншого висновку, а в виявленні основних

синтаксичних конструкцій початкової програми з метою побудови на їх основі

ланцюжків мови результуючої програми. У цьому сенсі типи нетермінальних

символів і цінні правила не несуть ніякої корисної інформації, а навпроти, тільки

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

знаходження остовного висновку є більш корисним ніж знаходження висновку на

основі початкової граматики. Знайдений остовний результат подальших

перетворень вже не потребує.

Page 85: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

85

У загальному вигляді послідовність побудови розпізнавача для КС-

граматики операторного передування G( VT,VN,P,S) можна описати слідуючим

чином:

1. На основі множини правил граматики Р побудувати множену крайніх

лівих і крайніх правих символів для всіх нетермінальних символів граматики.

2. На основі множини правил граматики Р і побудованих на кроці 1

множини побудувати множини крайніх лівих і крайніх правих термінальних

символів для всіх нетермінальних символів граматики.

3. На основі побудованих на кроці 2 множини для всіх термінальних

символів граматики заповнюється матриця операторного передування.

4. Початкова граматика G(VT,VN,P,S) переходить в остовну граматику з

одним нетермінальним символом.

5. На основі побудованої матриці передування та остовної граматики

будується розпізнавач на базі, алгоритму «сзсув-згортка», для граматик

операторного передування.

Важливо, що алгоритм розпізнавача може бути реалізований незалежно від

матриці передування і правил початкової граматики. Тоді, міняючи матрицю і

правила, один і той же алгоритм можна використовувати для розпізнавання

вхідних ланцюжків будь-якої граматики операторного передування.

Page 86: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

86

Змістовний модуль 5. Об’єктно – орієнтоване програмування. Класи. Конструктори, деструктори.

Основні розділи теми.

1. Об’єктно – орієнтоване програмування.

2. Означення класу.

3. Конструктори, деструктори.

4. Перевантаження операцій.

Питання для самоперевірки.

1. Що таке ООП?

2. Що таке поліморфізм?

3. Що таке спадкоємство?

4. Що таке інкапсуляція?

5. Коли використовується поіменована область?

6. Що таке клас? Складові класу.

7. Як оголошується клас?

8. Що називається методом?

9. Спеціфікатори доступу.

10. Як описати об’єкт?

11. Вказівник this. Як він використовуюється?

12. Призначення і основні властивості конструкторів?

13. Конструктор копіювання.

14. Статичні елементи класу.

15. Дружні функції і класи.

16. Призначення і основні властивості деструкторів.

17. Перевантаження операцій

18. Перевантаження бінарних операцій

19. Перевантаження операції присвоєння

20. Перевантаження операцій new і delete

21. Перевантаження операції приведення тип.

22. Перевантаження операції виклику функції

23. Перевантаження операції індексування

24. Вказівники на елементи класів Формат вказівника на метод

класу

25. Формат вказівника на поле класу

Література: 9 с. 140-163, с.173-221 7 с. 343-373, с. 416-450, с. 165-200.

Page 87: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

87

Об' єктно - орієнтоване програмування. Означення класу.

Конструктори, деструктори. Перевантаження операцій.

Поява ООП - реакція на кризу програмного забезпечення

ООП часто називають новою парадигмою програмування.

Красивий термін " парадигма" означає набір теорій, стандартів і методів, які

спільно є способом організації знань, - іншими словами, спосіб бачення світу. У

програмуванні цей термін використовується для визначення моделі обчислень,

тобто способу структуризації інформації, організації обчислень і даних.

Спосіб управління складними системами був відомий ще в давнину - divide

et impera(розділяй і володарюй). Тобто вихід - в декомпозиції системи на все

менші і менші підсистеми, кожну з яких можна удосконалювати незалежно.

Чому об'єктно-орієнтована декомпозиція виявилася ефективнішим засобом

боротьби із складністю процесів проектування і супроводу програмних систем,

чим функціональна декомпозиція?

Критерії якості декомпозиції проекту.

1. Із складністю додатка важко що-небудь зробити - вона визначається

метою створення програми. А ось складність реалізації можна спробувати

контролювати. Перше питання, що виникає при декомпозиції : на які

компоненти(модулі, функції, класи) треба розбити програму? Очевидно, що із

зростанням числа компонентів складність програми росте, оскільки потрібна

кооперація, координація і комунікація між компонентами. Особливо негативні

наслідки невиправданого розбиття на компоненти, коли виявляються розділеними

дії, по суті тісно пов'язані між собою.

2. Друга проблема пов'язана з організацією взаємодії між компонентами.

Взаємодія спрощується і його легше узяти під контроль, якщо кожен компонент

розглядається як деякий "чорний ящик", внутрішній устрій якого невідомий, але

відомі виконувані ним функції, а також " входи" і " виходи" цього ящика. Вхід

компонента дозволяє ввести в нього значення деякої вхідної змінної, а вихід -

отримати значення деякої вихідної змінної. У програмуванні сукупність входів і

виходів чорного ящика визначає інтерфейс компонента. Інтерфейс реалізується як

набір деяких функцій(чи запитів до компонента), викликаючи які клієнт або

отримує якусь інформацію, або міняє стан компонента.

Модне нині слівце " клієнт" означає попросту компонент, якому

знадобилися послуги іншого компонента, що виконує в цьому випадку роль

сервера. Взаємовідношення клієнт/сервер насправді дуже старе і

використовувалось вже у рамках структурного програмування, коли функція-

клієнт користувалася послугами функції-сервера шляхом її виклику.

Для оцінки якості програмного проекту треба враховувати, окрім усіх

інших, наступні два показники:

1. Зчеплення(cohesion) усередині компонента - показник, що

характеризує міру взаємозв'язку окремих його частин. Простий приклад: якщо

усередині компонента вирішуються дві підзадачі, які легко можна розділити, то

компонент має слабке(погане) зчеплення.

2. Зв'язаність(coupling) між компонентами - показник, що описує

інтерфейс між компонентом-клієнтом і компонентом-сервером. Загальне число

Page 88: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

88

входів і виходів сервера є міра зв'язаності. Чим менше зв'язаності між двома

компонентами, тим простіше зрозуміти і відстежувати в майбутньому їх

взаємодію. А оскільки у великих проектах ці компоненти часто розробляються

різними людьми, то дуже важливо зменшувати зв'язаність між компонентами.

Чому у разі функціональної декомпозиції важко досягти слабкої зв'язаності

між компонентами?

1. Річ у тому, що інтерфейс між компонентами в такому проекті

реалізується або через глобальні змінні, або через механізм

формальних/фактичних параметрів. У складній програмній системі,

реалізованій у рамках структурної парадигми, практично неможливо

обійтися без зв'язку через глобальні структури даних, а це означає, що

фактично будь-яка функція у разі помилки може зіпсувати ці дані.

Подібні помилки дуже важко локалізуються в процесі відладки.

Додайте до цього головний біль для супроводжуючого програміста,

який повинен пам'ятати десятки(якщо не сотні) звернень до загальних

даних з різних частин проекту. Відповідно, модифікація існуючого

проекту у зв'язку з новими вимогами замовника також зажадає дуже

великої роботи, оскільки виникне необхідність перевірити вплив

внесених змін практично на усі компоненти проекту.

2. Іншою проблемою в проектах з функціональною декомпозицією було

" прокляття" загального глобального простору імен. Члени команди,

працюючої над проектом, повинні були витрачати чималі зусилля із

узгодження вживаних імен для своїх функцій, щоб вони були

унікальними у рамках усього проекту.

Що принесло з собою ООП

Першою відмінністю ООП, що впадає у вічі, від структурного

програмування є використання класів. Клас - це тип, визначуваний програмістом,

в якому об'єднуються структури даних і функції їх обробки.

Конкретні змінні типу даних " клас" називаються екземплярами класу, або

об'єктами. Програми, що розробляються на основі концепцій ООП, реалізують

алгоритми, що описують взаємодію між об'єктами.

Клас містить константи і змінні, що називаються полями, а також

виконувані над ними операції і функції. Функції класу називаються методами.

Передбачається, що доступ до полів класу можливий тільки через виклик

відповідних методів. Поля і методи є елементами, або членами класу.

Ефективним механізмом послаблення зв'язаності між компонентами у разі

об'єктно-орієнтованої декомпозиції являється так звана інкапсуляція.

Інкапсуляція - це обмеження доступу до даних і їх об'єднання з методами,

які обробляють ці дані. Доступ до окремих частин класу регулюються за

допомогою спеціальних ключових слів: public(відкрита частина), private(закрита

частина) і protected(захищена частина).

Методи, розташовані у відкритій частині, формують інтерфейс класу і

можуть вільно викликатися клієнтом через відповідний об'єкт класу. Клас містить

константи і змінні, що називаються полями, а також виконувані над ними операції

і функції. Функції класу називаються методами. Передбачається, що доступ до

Page 89: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

89

полів класу можливий тільки через виклик відповідних методів. Поля і методи є

елементами, або членами класу.

Ефективним механізмом послаблення зв'язаності між компонентами у разі

об'єктно-орієнтованої декомпозиції являється так звана інкапсуляція.

Інкапсуляція - це обмеження доступу до даних і доступ до закритої секції

класу можливий тільки з його власних методів, а до захищеної - з його власних

методів, а також з методів класів-нащадків.

Інкапсуляція підвищує надійність програм, запобігаючи неумисному

помилковому доступу до полів об'єкту. Окрім цього, програму легше

модифікувати, оскільки при збереженні інтерфейсу класу можна міняти його

реалізацію, і це не торкнеться зовнішнього програмного коду(код клієнта).

Помітимо, що клас обдаровує свого програміста-розробника надійним "

укриттям", забезпечуючи локальну(в межах класу) зону видимості імен. Тепер

можна скоротити штат бригади програмістів : фахівець, що відповідає за

узгодження імен функцій і імен глобальних структур даних між членами бригади,

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

можуть спокійнісінько мати однакові імена. Те ж відноситься і до полів різних

класів.

З ООП пов'язані ще два інструменти, грамотне використання яких підвищує

якість проектів : спадкоємство класів і поліморфізм.

Спадкоємство - механізм отримання нового класу з існуючого. Похідний

клас створюється шляхом доповнення або зміни існуючого класу. Завдяки цьому

реалізується концепція повторного використання коду. За допомогою

спадкоємства може бути створена ієрархія споріднених типів, які спільно

використовують код і інтерфейси.

Поліморфізм дає можливість створювати множинні визначення для

операцій і функцій. Яке саме визначення використовуватиметься, залежить від

контексту програми. Ви вже знайомі з одним з різновидів поліморфізму в мові

C++ - перевантаженням функцій. Програмування з класами надає ще дві

можливості: перевантаження операцій і використання так званих віртуальних

методів.

Перевантаження операцій дозволяє застосовувати для власних класів ті ж

операції, які використовуються для вбудованих типів C++.

Віртуальні методи забезпечують можливість вибрати на етапі виконання

потрібний метод серед однойменних методів базового і похідного класів.

Окрім спадкоємства, класи можуть знаходитися також відносно агрегації:

наприклад, коли у складі одного класу є об'єкти іншого класу. Спільне

використання спадкоємства, композиції і поліморфізму лежить в основі

елегантних проектних рішень, що забезпечують найбільшу простоту модифікації

програми.

Простори імен(пойменовані області)

Пойменовані області служать для логічного групування оголошень і

обмеження доступу до них. Чим більше програми, тим актуальніше використання

пойменованих областей. Простим прикладом застосування є відділення коду,

написаного однією людиною, від коду, написаного іншим. При використанні

Page 90: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

90

єдиної глобальної зони видимості формувати програму з окремих частин дуже

складно із-за можливого збігу і конфлікту імен. Використання пойменованих

областей перешкоджає доступу до непотрібних засобів.

Оголошення пойменованої області(її також називають простором імен) має

формат:

namespace [ ім’я_області ]{ /* Об’явлення */ }

Пойменована область може оголошуватися неодноразово, причому

подальші оголошення розглядаються як розширення попередніх. Таким чином,

пойменована область може оголошуватися і змінюватися за рамками одного

файлу.

Якщо ім'я області не задане, компілятор визначає його самостійно за

допомогою унікального ідентифікатора, різного для кожного модуля. Оголошення

об'єкту в неіменованій області рівнозначне його опису як глобального з

модифікатором static. Поміщати оголошення в таку область корисно для того, щоб

зберегти локальність коду. Не можна отримати доступ з одного файлу до елементу

неіменованої області іншого файлу.

Приклад.

namespace demo{

int i = 1; int k = 0;

void fund (int);

void func2(int) { /* ... */ }

}

namespace demo{ // Розширення

// int i = 2; Невірно – подвійне визначення

void funcl(double); // Перегрузка

void func2(int); // Вірно (повторне оголошення)

}

У оголошенні пойменованої області можуть бути присутніми як

оголошення, так і визначення. Логічно поміщати в неї тільки оголошення, а

визначати їх пізніше за допомогою імені області і оператора доступу до зони

видимості ::, наприклад:

void demo::fund(int) { /* ... */ }

Це застосовується для розділення інтерфейсу і реалізації. У такий спосіб не

можна оголосити новий елемент простору імен.

Об'єкти, оголошені усередині області, є видимими з моменту оголошення.

До них можна явно звертатися за допомогою імені області і оператора доступу до

зони видимості ::, наприклад:

demo::i = 100; demo::func2(10);

Якщо ім'я часто використовується поза своїм простором, можна оголосити

його доступним за допомогою оператора using :

using demo::i: Після цього можна використати ім'я без явної вказівки області.

Якщо вимагається зробити доступними усі імена з якої-небудь області,

використовується оператор using namespace :

using namespace demo:

Page 91: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

91

Оператори using і using namespace можна використати і усередині

оголошення пойменованої області, щоб зробити в ній доступними оголошення з

іншої області:

namespace Department _ of _ Applied _ Mathematics{ using demo::i; // ... }

Імена, оголошені в пойменованій області явно або за допомогою оператора

using, мають пріоритет по відношенню до імен, оголошених за допомогою

оператора usi ng namespace(це має значення при включенні декількох

пойменованих областей, що містять співпадаючі імена).

Короткі імена просторів імен можуть увійти до конфлікту один з одним, а

довгі непрактичні при написанні реального коду, тому допускається вводити

синоніми імен :

namespace DAM = Department _ of _ Applied _ Mathematics;

Простори імен стандартної бібліотеки. Об'єкти стандартної бібліотеки

визначені в просторі імен std. Наприклад, оголошення стандартних засобів

введення/виведення.В заголовному файлі <stdio.h> поміщені в простір імен таким

чином:

// stdio.h namespace std{

int feof(FILE *f);

}

using namespace std;

Це забезпечує сумісність зверху вниз. Для тих, хто не бажає присутності

неявно доступних імен, визначений новий заголовний файл <cstdio>:

// cstdio

namespace std{

int feof(FILE *f);

}

Якщо в програму включений файл <cstdio>, треба вказувати ім'я простору

імен явним чином :

std::feof(f);

Механізм просторів імен разом з директивою finclude забезпечують

необхідну при написанні великих програм гнучкість шляхом поєднання логічного

групування пов'язаних величин і обмеження доступу.

Як правило, в будь – якому функціонально завершеному фрагменті

програми можна виділити інтерфейсну частину(наприклад, заголовки функцій,

опису типів), необхідну для використання цього фрагмента, і частину реалізації,

тобто допоміжні змінні, функції і інші засоби, доступ до яких ззовні не потрібно.

Простори імен дозволяють приховати деталі реалізації і, отже, спростити

структуру програми і зменшити кількість потенційних помилок. Продумане

розбиття програми на модулі, чітка специфікація інтерфейсів і обмеження доступу

дозволяють організувати ефективну роботу над проектом групи програмістів.

Класи.

Опис класу

Клас є абстрактним типом даних, визначуваним користувачем, і є моделлю

реального об'єкту у вигляді даних і функцій для роботи з ними.

Page 92: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

92

Дані класу називаються полями(по аналогії з полями структури), а функції

класу - методами. Поля і методи називаються елементами класу. Опис класу в

першому наближенні виглядає так:

class <ім'я>

{

[ private: ]

<опис прихованих елементів>

public:

<опис доступних елементів>

}; // Опис закінчується крапкою з комою

Специфікатори доступу private і public управляють видимістю елементів

класу. Елементи, описані після службового слова private, видимі тільки усередині

класу. Цього вигляду доступу набраний в класі за замовчуванням. Інтерфейс класу

описується після специфікатора public. Дія будь-якого специфікатора

поширюється до наступного специфікатора або до кінця класу. Можна задавати

декілька секцій private і public, порядок їх дотримання значення не має.

У літературі частіше зустрічаються терміни "дані-члени" і "функції-члени",

а також "компонентні дані" і "компонентні функції".

Поля класу :

можуть мати будь-який тип, окрім типу цього ж класу(але можуть

бути вказівниками або посиланнями на цей клас);

можуть бути описані з модифікатором const, при цьому вони

ініціалізувалися тільки один раз(за допомогою конструктора) і не

можуть змінюватися;

можуть бути описані з модифікатором static, але не як auto, extern і

register,

Класи можуть бути глобальними(оголошеними поза будь-яким блоком) і

локальними(оголошеними усередині блоку, наприклад, функції або іншого класу).

Особливості локального класу :

усередині локального класу можна використати типи, статичні(static) і

зовнішні(extern) змінні, зовнішні функції і елементи перерахувань з

області, в якій він описаний;

забороняється використати автоматичні змінні з цієї області;

локальний клас не може мати статичних елементів

методи цього класу можуть бути описані тільки усередині класу;

якщо один клас вкладений в інший клас, вони не мають яких-небудь

особливих прав доступу до елементів один одного і можуть звертатися

до них тільки за загальними правилами.

Приклад: створимо клас, моделюючий персонаж комп'ютерної гри. Для

цього вимагається задати його властивості(наприклад, кількість щупалець, силу

або наявність гранатомета) і поведінку. Приклад буде схемний, оскільки

наводиться лише для демонстрації синтаксису.

class monstr

{

int health, ammo;

Page 93: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

93

public:

monstr(int he = 100, int am = 10){ health = he; ammo = am;}

void draw(int x, int y, int scale, int position) :

int get _ health(){return health:}

int get _ ammo(){return ammo:}

};

У цьому класі два приховані поля - health і ammo, отримати значення яких

ззовні можна за допомогою методів get _ health() і get _ ammo().

Усі методи класу мають безпосередній доступ до його прихованих полів,

іншими словами, тіла функцій класу входять в зону видимості private елементів

класу.

У приведеному класі міститься три визначення методів і одно

оголошення(метод draw). Якщо тіло методу визначене усередині класу, він є

вбудованим(inline). Як правило, вбудованими роблять короткі методи. Якщо

усередині класу записано тільки оголошення(заголовок) методу, сам метод має

бути визначений у іншому місці програми за допомогою операції доступу до зони

видимості(::) :

void monstr::draw(int х, int у, int scale, int position)

{

/* тіло методу */

}

Метод можна визначити як вбудований і поза класом за допомогою

директиви inline(як і для звичайних функцій, вона носить рекомендаційний

характер) :

inline int monstr::get _ ammo()

{

return ammo:

}

У кожному класі є хоч би один метод, ім'я якого співпадає з ім'ям класу. Він

називається конструктором і викликається автоматично при створенні об'єкту

класу. Конструктор призначений для ініціалізації об'єкту. Автоматичний виклик

конструктора дозволяє уникнути помилок, пов'язаних з використанням

неініціалізованих змінних.

Опис об'єктів

Конкретні змінні типу " клас" називаються екземплярами класу, або

об'єктами. Час життя і видимість об'єктів залежить від виду і місця їх опису і

підкоряється загальним правилам З++:

monstr Vasia: // Об'єкт класу monstr з параметрами за замовчуванням

monstr Super(200, 300); // Об'єкт з явною ініціалізацією

monstr stado[100]; // Масив об'єктів з параметрами за замовчуванням

monstr *beavis = new monstr(10); //

При створенні кожного об'єкту виділяється пам'ять, достатня для зберігання

усіх полів, і автоматично викликається конструктор, що виконує їх ініціалізацію.

Методи класу не тиражуються. При виході об'єкту з області дії він знищується,

при цьому автоматично викликається деструкція.

Page 94: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

94

Доступ до елементів об'єкту аналогічний доступу до полів структури. Для

цього використовуються операція . (точка) при зверненні до елементу через ім'я

об'єкту і операція -> при зверненні через вказівник, наприклад:

int n = Vasia.get _ ammo();

stado[5].draw;

cout " beavis ->get _ health();

Звернутися таким чином можна тільки до елементів із специфікаторам

public.

Отримати або змінити значення елементів із специфікатором private можна

тільки через звернення до відповідних методів.

Можна створити константний об'єкт, значення полів якого змінювати

забороняється. До нього повинні застосовуватися тільки константні методи:

class monstr

{ int get_health() const {return health;}

}:

const monstr Dead(0,0); // Константний об’єкт

cout « Dead.get_health();

Константний метод:

оголошується з ключовим словом const після списку параметрів;

не може змінювати значення полів класу;

може викликати тільки константні методи;

може викликатися для будь-яких(не лише константних) об'єктів.

Рекомендується описувати як константні ті методи, які призначені для

набуття значень полів.

Вказівник this

Кожен об'єкт містить свій екземпляр полів класу. Методи класу знаходяться

в пам'яті в єдиному екземплярі і використовуються усіма об'єктами спільно, тому

необхідно забезпечити роботу методів з полями саме того об'єкту, для якого вони

були викликані. Це забезпечується передачею у функцію прихованого параметра

this, в якому зберігається константний вказівник на того, що викликав функцію

об'єкт. Вказівник this неявно використовується усередині методу для посилань на

елементи об'єкту. У явному виді цей вказівник застосовується в основному для

повернення з методу вказівника(return this;) або посилання

(return *this;) на названий об’єкт.

Для ілюстрації використання вказівника this додамо в приведений вище клас

monstr новий метод, що повертає посилання на найбільш здорового(поле health) з

двох монстрів, один з яких викликає метод, а інший передається йому в якості

параметра(метод треба помістити в секцію public опису класу) :

monstr & the _ best(monstr &М)

{

if( health > М.health) return this;

return M;

}

... monstr Vasia(50), Super(200);

Page 95: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

95

// Новий об'єкт Best ініціалізувався значеннями полів Super :

monstr Best = Vasia.the _ best(Super) :

Вказівник this можна також застосовувати для ідентифікації поля класу у

тому випадку, коли його ім'я співпадає з ім'ям формального параметра методу.

Інший спосіб ідентифікації поля використовує операцію доступу до зони

видимості :

void cure(int health, int ammo)

{

this -> health += health; // Використання this

monstr:: ammo += ammo; // Використання операції ::

}

Конструктори

Конструктор призначений для ініціалізації об'єкту і викликається

автоматично при його створенні.

Основні властивості конструкторів.

Конструктор не повертає значення, навіть типу void. Не можна

отримати вказівник на конструктор.

Клас може мати декілька конструкторів з різними параметрами для

різних видів ініціалізації(при цьому використовується механізм

перевантаження).

Конструктор, що викликається без параметрів, називається

конструктором за замовчуванням.

Параметри конструктора можуть мати будь-який тип, окрім цього ж

класу. Можна задавати значення параметрів за замовчуванням . Їх

може утримувати тільки один з конструкторів.

Якщо програміст не вказав жодного конструктора, компілятор

створює його автоматично. Такий конструктор викликає конструктори

за замовчуванням для полів класу і конструктори за замовчуванням

базових класів У разі, коли клас містить константи або посилання, при

спробі створення об'єкту класу буде видана помилка, оскільки їх

необхідно ініціалізувати конкретними значеннями, а конструктор за

замовчуванням цього робити не уміє.

Конструктори не наслідують.

Конструктори не можна описувати з модифікаторами const. virtual і

static.

Конструктори глобальних об'єктів викликаються до виклику функції

main. Локальні об'єкти створюються, як тільки стає активною зона їх

дії. Конструктор запускається і при створенні тимчасового

об'єкту(наприклад, при передачі об'єкту з функції).

Конструктор викликається, якщо в програмі зустрілася яка-небудь з

синтаксичних конструкцій:

Ім’я_класа ім’я _об’єкта [(список параметрів)];

Page 96: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

96

// Список параметрів не має бути порожнім

Ім’я_класа(список параметрів);

// Створюється об'єкт без імені(список може бути порожнім)

Ім’я_класа Ім’я_ об’єкта = вираження;

// Створюється об'єкт без імені і копіюється

Приклади:

monstr Super(200, 300), Vas1a(50), Z;

monstr X = monstr(1000);

monstr Y = 500;

У першому операторові створюються три об'єкти. Значення не вказаних

параметрів встановлюються за замовчуванням.

У другому операторові створюється безіменний об'єкт зі значенням

параметра health = 1000(значення другого параметра встановлюється за

замовчуванням). Виділяється пам'ять під об'єкт X, в яку копіюється безіменний

об'єкт.

У останньому операторові створюється безіменний об'єкт зі значенням

параметра health = 500(значення другого параметра встановлюється за

замовчуванням). Виділяється пам'ять під об'єкт Y, в яку копіюється безіменний

об'єкт. Така форма створення об'єкту можлива у тому випадку, якщо для

ініціалізації об'єкту допускається задати один параметр.

Приклад класу з декількома конструкторами:

enum color {red, green, blue}; // Можливі значення кольору

class monstr

{

int health, ammo;

color skin;

char *name;

public:

monstr(int he = 100, int am =10);

monstr(color sk);

monstr(char * nam);

int get_health(){return health;}

int get_ammo(){return ammo;}

};

//---- - --

monstr::monstr(int he, int am)

{

health = he; ammo = am; skin = red; name = 0;

}

//---- -

monstr::monstr(color sk)

{

switch (sk)

{

Page 97: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

97

case red: health = 100; ammo = 10; skin = red; name = 0; break;

case green: health = 100; ammo = 20; skin =green; name = 0; break;

case blue: health = 100; ammo = 40; skin = blue; name = 0; break;

}

}

// - - - - - - - - - - -

monstr::monstr(char * nam)

{

name = new char [strlen(nam) + 1];

// К довжині строки добавляється 1 для зберігання нуль-символ

strcpy(name, nam);

health = 100; ammo =10; sk1n = red:

}

//----

monstr * m = new monstr ("Ork");

monstr Green (green);

Перший з приведених вище конструкторів є конструктором за

замовчуванням, оскільки його можна викликати без параметрів. Об'єкти класу

monstr тепер можна ініціалізувати різними способами, необхідний конструктор

буде викликаний залежно від списку значень в дужках. При завданні декількох

конструкторів слід дотримуватися тих же правил, що і при написанні

переобтяжених функцій - у компілятора має бути можливість розпізнати

потрібний варіант.

ПРИМІТКА

Перевантажувати можна не лише конструктори, але і інші методи класу.

Існує ще один спосіб ініціалізації полів в конструкторі(окрім використаного

в приведеній вище програмі привласнення полям значень формальних параметрів)

- за допомогою списку ініціалізаторів, розташованих після двокрапки між

заголовком і тілом конструктора :

monstr::monstr(int he; int am):

health (he), ammo (am), skin (red), name (0){}

Поля перераховуються через кому. Для кожного поля в дужках вказується

значення, що ініціалізувало, яке може бути вираженням. Без цього способу не

обійтися при ініціалізації полів-констант, полів-посилань і полів-об'єктів, В

останньому випадку буде викликаний конструктор, що відповідає вказаним в

дужках параметрам.

ПРИМІТКА

Конструктор не може повернути значення, щоб повідомити про помилку під

час ініціалізації. Для цього можна використати механізм обробки виняткових

ситуацій.

Конструктор копіювання

Конструктор копіювання - це спеціальний вид конструктора, одержуючий

в якості єдиного параметра вказівник на об'єкт цього ж класу :

T::T(const Т&) { ... /* Тіло конструктора */ }

де Т ~ ім'я класу.

Page 98: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

98

Цей конструктор викликається в тих випадках, коли новий об'єкт

створюється шляхом копіювання існуючого :

при описі нового об'єкту з ініціалізацією іншим об'єктом;

при передачі об'єкту у функцію за значенням;

при поверненні об’єкту з функції

Якщо програміст не вказав жодного конструктора копіювання, компілятор

створює його автоматично. Такий конструктор виконує по елементне копіювання

полів. Якщо клас містить вказівники або посилання, це, швидше за все, буде

неправильним, оскільки і копія, і оригінал вказуватимуть на одну і ту ж область

пам'яті.

Запишемо конструктор копіювання для класу monstr. Оскільки в нім є поле

name, що утримує вказівник на рядку символів, конструктор копіювання повинен

виділяти пам'ять під новий рядок і копіювати в неї початкову:

monstr::monstr(const monstr &М)

{

if (M.name)

{

name = new char [strlen(M.name) + 1]:

strcpy(name, M.name):

}

else name = 0;

health = M.health; ammo = M.ammo; skin = M.skin;

}

monstr Vasia (blue);

monstr Super = Vasia; // Працює конструктор копіювання

monstr *m = new monstr ("Orс");

monstr Green = *m; // Працює конструктор копіювання

ПРИМІТКА

Будь-який конструктор класу, що приймає один параметр якого-небудь

іншого типу, називається конструктором перетворення, оскільки він здійснює

перетворення з типу параметра в тип цього класу.

Статичні елементи класу

За допомогою модифікатора static можна описати статичні поля і методи

класу. Їх можна розглядати як глобальні змінні або функції, доступні тільки в

межах області класу.

Статичні поля

Статичні поля застосовуються для зберігання даних, загальних для усіх

об'єктів класу, наприклад, кількості об'єктів або посилання на того, що

розділяється усіма об'єктами ресурс. Ці поля існують для усіх об'єктів класу в

єдиному екземплярі, тобто не дублюються.

Особливості статичних полів :

Пам'ять під статичне поле виділяється один раз при його ініціалізації

незалежно від числа створених об'єктів(і навіть при їх відсутності) і

ініціалізувався за допомогою операції доступу до зони дії, а не

операції вибору (визначення має бути записане зовні функції)

Page 99: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

99

class А

{

public:

static int count: // Оголошення в класі

};

int A::count; // ОВизначення в глобальній області

// За замовчуванням ініціалізується нулем

int A::count = 10; // Приклад ініціалізації випадковим значенням

Статичні поля доступні як через ім'я класу, так і через ім'я об'єкту : А

*а, b; .

cout " A::count " a ->count " b.count:

// Буде виведено одно і те ж

На статичні поля поширюється дія специфікаторів доступу, тому

статичні поля, описані як private, не можна змінити за допомогою

операції доступу до зони дії, як описано вище. Це можна зробити

тільки за допомогою статичних методів.

Пам'ять, займана статичним полем, не враховується при визначенні

розміру об'єкту за допомогою операції sizeof.

Статичні методи

Статичні методи призначені для звернення до статичних полів класу. Вони

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

інші статичні методи класу, тому що їм не передається прихований вказівник this.

Звернення до статичних методів робиться так само, як до статичних полів - або

через ім'я класу, або, якщо хоч би один об'єкт класу вже створений, через ім'я

об'єкту.

class А

{

static int count: // Поле count - приховане

public: static void inc_count(){ count++; }

};

A:: int count; // Визначення в глобальній області

void f()

{

А а;

a.count++; // не можна, поле count приховане

// Зміна поля за допомогою статистичного методу;

a.inc_count(); // або

А::inc__count();

}

Статичні методи не можуть бути константними(const) і віртуальними

(virtual).

Page 100: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

100

Дружні функції і класи

Іноді бажано мати безпосередній доступ ззовні до прихованих полів класу,

тобто розширити інтерфейс класу. Для цього служать дружні функції і дружні

класи.

Дружня функція

Дружні функції застосовуються для доступу до прихованих нулів класу і є

альтернативою методам. Метод, як правило, використовується для реалізації

властивостей об'єкту, а у вигляді дружніх функцій оформляються дії, що не

представляють властивості класу, але що концептуально входять в його інтерфейс

і потребують доступу до його прихованих полів, наприклад, перевизначені

операції виведення об'єктів.

Правила опису і особливості дружніх функцій.

Дружня функція оголошується усередині класу, до елементів якого їй

потрібний доступ, з ключовим словом friend. В якості параметра їй

повинні передаватися об'єкт або посилання на об'єкт класу, оскільки

вказівник this їй не передається.

Дружня функція може бути звичайною функцією або методом іншого

раніше певного класу. На неї не поширюється дія специфікаторів

доступу, місце розміщення її оголошення в класі байдуже.

Одна функція може бути дружньою відразу декільком класам.

Приклад опис двох функцій, дружніх класу monstr. Функція kill є методом

класу hero, а функція steal _ ammo не належить жодному класу. Обом функціям в

якості параметра передається посилання на об'єкт класу monstr.

class monstr; // Попереднє проголошення класу

class hero

{

public:

void kill(monstr &);

….

};

class monstr

{

friend int steal_ammo(monstr &);

friend void hero::kill(monstr &);

//Класс hero повинен були проголошений раніше

};

int steal_ammo(monstr &M){return --M.ammo;}

void hero::kill(monstr &M){M.health = 0; M.ammo = 0;}

Використання дружніх функцій треба по можливості уникати, оскільки вони

порушують принцип інкапсуляції і, таким чином, утрудняють відладку і

модифікацію програми.

Page 101: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

101

Дружній клас

Якщо усі методи якого-небудь класу повинні мати доступ до прихованих

полів іншого, увесь клас оголошується дружнім за допомогою ключового слова

friend.

Приклад класу mistress дружнього класу hero :

class hero

{

friend class mistress:

}

class mistress

{

void f1();

void f2();

}

Функції f1 і f2 є дружніми по відношенню до класу hero(хоча і описані без

ключового слова friend) і мають доступ до усіх його полів.

Оголошення friend не є специфікатором доступу і не наслідує.

ПРИМІТКА

Зверніть увагу на те, що клас сам визначає, які функції і класи є дружніми, а

які немає.

Деструкції

Деструкція - це особливий вид методу, що застосовується для звільнення

пам'яті, займаної об'єктом. Деструкція викликається автоматично, коли об'єкт

виходить з області видимості:

для локальних об'єктів - при виході з блоку, в якому вони оголошені;

для глобальних - як частину процедури виходу з main:

для об'єктів, заданих через вказівники, деструкція викликається

неявно при використанні операції delete.

УВАГА

При виході з області дії вказівника на об'єкт автоматичний виклик

деструкції об'єкту не робиться.

Ім'я деструкції розпочинається з тильди(~), безпосередньо за якою йде ім'я

класу.

Деструкція:

не має аргументів і поверненого значення;

не може бути оголошений як const або static;

не наслідує;

може бути віртуальним.

Якщо деструкція явним чином не визначена, компілятор автоматично

створює порожню деструкцію.

Описувати в класі деструкцію явним чином вимагається у разі, коли об'єкт

містить вказівники на пам'ять, що виділяється динамічно ~ інакше при знищенні

об'єкту пам'ять, на яку посилалися його поля-вказівники, не буде помічена як

вільна.

Вказівник на деструкцію визначити не можна.

Page 102: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

102

Приклад деструкції :

monstr::~monstr(){delete [] name;}

Деструкцію можна викликати явним чином шляхом вказівки повністю

уточненого імені, наприклад:

monstr *m; ...

m -> ~monstr();

Це може знадобитися для об'єктів, яким за допомогою переобтяженої

операції new виділялася конкретна адреса пам'яті. Без необхідності явно

викликати деструкцію об'єкту не рекомендується.

Перевантаження операцій

C + + дозволяє перевизначити дію більшості операцій так, щоб при

використанні з об'єктами конкретного класу вони виконували задані функції. Ця

дає можливість використовувати власні типи даних точно так само, як стандартні.

Позначення власних операцій вводити не можна. Можна перевантажувати будь-

які операції, що існують в C + +, за винятком:

. *? ::: # # # Sizeof

Перевантаження операцій здійснюється за допомогою методів

спеціального виду (функцій-операцій) і підпорядковується наступним правилам:

при перевантаженні операцій зберігаються кількість аргументів, пріоритети

операцій та правила асоціації (справа наліво або зліва направо), що

використовуються в стандартних типах даних;

для стандартних типів даних перевизначати операції не можна;

функції-операції не можуть мати аргументів за замовчуванням;

функції-операції успадковуються (за винятком =);

функції-операції не можуть визначатися як static.

Функцію-операцію можна визначити трьома способами:

o вона повинна бути або методом класу,

o або дружньою функцією класу,

o або звичайною функцією.

У двох останніх випадках функція повинна приймати хоча б один аргумент, який

має тип класу, вказівника або посилання на клас.

Функція-операція містить ключове слово operator, за яким слід знак

перевизначають операції:

тип operator операція (список параметрів) {тіло функції}

Унарний функція-операція, яка визначається всередині класу, повинна бути

представлена за допомогою нестатичні методу без параметрів, при цьому

операндом є викликав її об'єкт, наприклад:

Page 103: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

103

class monstr

{

monstr & operator + + () {+ + health; return * this;}

}:

monstr Vasia;

cout «(+ + Vasia). get_health ();

Якщо функція визначається поза класом, вона повинна мати один параметр типу

класу:

class monstr

{

friend monstr & operator + + (monstr & M);

};

monstr & operator + + (monstr & M) {+ + M.health; return M;}

Якщо не описувати функцію всередині класу як дружню, потрібно враховувати

доступність змінюваних полів. В даному випадку поле health недоступно ззовні,

так як описано з специфікатором private, тому для його зміни потрібне

використання відповідного методу. Введемо в опис класу monstr метод

change_health, що дозволяє змінити значення поля health

void change__health (int he) {health = he;}

Тоді можна перевантажити операцію інкремента за допомогою звичайної

функції, описаної поза класом:

monstr & operator + + (monstr & М)

{

int h = M.get__health (); h + +;

M.change_health (h);

return М;

}

Особливий випадок: функція-операція, перший параметр якої має стандартний

тип, не може визначатися як метод класу.

Операції постфіксной інкремента і декремента повинні мати перший параметр

типу int. Він використовується тільки для того, щоб відрізнити їх від префіксной

форми:

Page 104: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

104

class monstr

{

...

monstr operator + + (int)

{

monstr M (* this); health + +;

return M;

}

}:

monstr Vasia;

cout «(Vas1a + +). get_health ();

Перевантаження бінарних операцій

Бінарна функція-операція, яка визначається всередині класу, повинна бути

представлена за допомогою нестатичного методу з параметрами, при цьому

викликав її об'єкт вважається першим операндом:

class monstr

{

...

bool operator> (const monstr & M)

{

if (health> M.health) return true;

return false;

}

}:

Якщо функція визначається поза класом, вона повинна мати два параметри типу

класу:

bool operator> (const monstr & M1, const monstr & M2)

{

if (Ml.get_health ()> M2.get_health ()) return true;

return false;

Page 105: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

105

}

Перевантаження операції присвоєння

Операція присвоювання визначена в будь-якому класі за замовчуванням як

поелементне копіювання. Ця операція викликається кожного разу, коли одному

існуючого об'єкту присвоюється значення іншого. Якщо клас містить поля,

пам'ять під які виділяється динамічно, необхідно визначити власну операцію

привласнення. Щоб зберегти семантику привласнення, операція-функція повинна

повертати посилання на об'єкт, для якого вона викликана, і приймати як параметр

єдиний аргумент - посилання на присвоюється об'єкт.

const monstr & operator = (const monstr & M)

{

Перевірка на само присвоювання:

if (& М == this) return * this;

if (name) delete [] name;

if (M.name)

{

name = new char [strlen (M.name) + 1];

strcpy (name. M.name);

}

else name = 0;

health = M.health; ammo = M.ammo; skin = M.skin;

return * this;

}

Повернення з функції вказівника на об'єкт робить можливою ланцюжок операцій

присвоювання:

monstr А (10). В. С;

С = В = А;

Операцію присвоювання можна визначати лише як метод класу. Вона не

успадковується.

Перевантаження операцій new і delete

Page 106: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

106

Щоб забезпечити альтернативні варіанти управління пам'яттю, можна визначати

власні варіанти операцій new і new [] для виділення динамічної пам'яті під об'єкт і

масив об'єктів відповідно, а також операції delete і delete [] для її звільнення.

Ці функції-операції повинні відповідати таким правилам:

їм не потрібно передавати параметр типу класу;

першим параметром функцій new і new [] повинен передаватися розмір

об'єкта типу size_t (це тип, що повертається операцією sizeof, він

визначається в заголовному файлі <stddef.h>); при виклику він передається

у функції неявним чином;

вони повинні визначатися з типом значення, що повертається void *, навіть

якщо return повертає вказівник на інші типи (найчастіше на клас);

операція delete повинна мати тип повернення void і перший аргумент типу

void *; операції виділення і звільнення пам'яті є статичними елементами класу.

Поведінка перевантажених операцій має відповідати діям, що виконуються ними

за замовчуванням. Для операції new це означає, що вона повинна повертати

правильне значення, коректно обробляти запит на виділення пам'яті нульового

розміру і породжувати виняток при неможливості виконання запиту.

Для операції delete слід дотримуватися умови, для видалення нульового

вказівника має бути безпечною, тому всередині операції необхідна перевірка

вказівника на нуль і відсутність будь-яких дій в разі рівності.

Стандартні операції виділення і звільнення пам'яті можуть використовуватися в

області дії класу поряд з перевантаженими (за допомогою операції доступу до

області видимості :: для об'єктів цього класу і безпосередньо - для будь-яких

інших).

Перевантаження операції виділення пам'яті застосовується для економії пам'яті,

підвищення швидкодії програми або для розміщення даних в деякій конкретній

області.

Наприклад, нехай описується клас, що містить вказівник на деякий об'єкт:

class Obj {...};

class pObj

{

private:

Obj * p:

};

Page 107: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

107

При виділенні пам'яті під об'єкт типу pObj за допомогою стандартної операції

new

pObj * р = new pObj;

фактичну кількість байтів буде перевищувати sizeof (pObj), оскільки new

зазвичай записує в початок виділеної області її розмір (для того щоб правильно

відпрацьовувала операція delete):

Для невеликих об'єктів ці накладні витрати можуть виявитися досить значними.

Для економії пам'яті можна написати власну операцію new класу pObj, яка

виділятиме великий блок пам'яті, а потім розміщувати в ньому вказівники на Obj.

Для цього в об'єкт pObj вводиться статичне поле headOfFree, в якому зберігається

вказівник на першу вільну комірку блоку для розміщення чергового об'єкта.

Невикористані осередку зв'язуються в список. Щоб не займати пам'ять під поле

зв'язку, використовується об'єднання (union), за допомогою якого одна і та ж

осередок використовується або для розміщення вказівника на об'єкт, або для

зв'язку з наступного вільної осередком:

Звідси

class pObj {

public:

static void * operator new (size_t size):

private:

union {

Obj * p: / / Вказівник на об'єкт

pObj * next: / / Вказівник на наступну вільну комірку

}:

static const int BLOCK__SIZE :/ / Розмір блоку

Заголовок списку вільних осередків:

static pObj * headOfFree:

}:

void * pObj :: operator new (size_t size) {

Переспрямувати запити невірного кількості пам'яті

Стандартної операції new:

if (size! = sizeof (pObj)) return :: operator new (size);

Page 108: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

108

pObj * p = headOfFree; / / Вказівник на першу вільну комірку

Перемістити вказівник списку вільних осередків:

if (р) headOfFree = р -> next:

Якщо вільної пам'яті немає. виділяємо черговий блок:

else {

pObj * newblock = static__cast <pObj*> ^

(:: Operator new (BLOCK__SIZE * sizeof (pObj))):

Bcі осередки вільні, крім першого (з / / Зайнята), пов'язуємо їх:

for (int i - 1: i <BLOCKJIZE - 1: + + i)

newblock [i]. next = & newblock [i +1]:

newblock [BLOCK__SIZE - l]. next = 0:

Встановлюємо початок списку вільних осередків:

headOfFree = & newblock [l]:

р = newblock:

}

Повертаємо вказівник на виділену пам'ять

}

Перевантажена операція new успадковується, тому вона викликається для

похідних об'єктів. Якщо їх розмір не відповідає розміру базового (а так, швидше за

все, і є), це може викликати проблеми. Щоб їх уникнути, на початку операції

перевіряється відповідність розмірів. Якщо розмір об'єкта не дорівнює тому, для

якого перевантажена операція new, запит на виділення пам'яті передається

стандартної операції new.

У програмі, що використовує клас pObj, повинна бути присутнім ініціалізація

його статичних полів.

pObj * pObj :: headOfFree: / / Встановлюється в О за замовчуванням

const int pObj :: BLOCK_SIZE = 1024:

Як видно з цього прикладу, крім економії пам'яті досягається ще й високу

швидкодію, адже в більшості випадків виділення пам'яті зводиться до декількох

простих операторів.

Природно, що якщо операція new перевантажена, те ж саме повинно бути

виконано і для операції delete (наприклад, в нашому випадку стандартна операція

Page 109: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

109

delete не знайде на початку об'єкта вірної інформації про його розміри, що

призведе до невизначеного поведінки програми).

У розглянутому прикладі операція delete повинна додавати звільнений елемент

пам'яті до списку вільних осередків:

void pObj :: operator delete (void * ObjToDie. size ^ t size) {

if (ObjToDie * == 0) return:

if (size! = sizeof (pObj)) {

Тут використано явне перетворення типу за допомогою операції staticcast.

:: Operator delete (ObjToDie): return:

}

pObj * p = stat1c_cast <p0bj*> (0bjToDie);

p-> next = headOfFree;

headOfFree = p;

}

В операції delete виконана перевірка відповідності розмірів об'єктів, аналогічна

наведеної в операції new.

Перевантаження операції приведення типу

Можна визначити функції-операції, які будуть здійснювати перетворення об'єкта

класу до іншого типу. Формат:

operator імя _нового ^ типу В:

Приклад:

. Monstr :: operator 1nt () {return health;}

monstr Vasia; cout «intCVasia):

Перевантаження операції виклику функції

Клас, в якому визначена операція виклику функції, називається функціональним.

Від такого класу не потрібно наявності інших полів і методів:

class if_greater {

public:

1nt operator О (1nt a. Int b) const {

return a> b:

Page 110: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

110

}

}:

Використання такого класу має досить специфічний синтаксис. Розглянемо

приклад:

if_greater х:

cout «х (1. 5)« endl; / / Результат - О

cout «1f__greater () (5. 1)« endl; / / Результат - 1

Оскільки в класі if_greater визначена операція виклику функції з двома

параметрами, вираз х (1, 5) є допустимим (те ж саме можна запит Як видно з

прикладу, об'єкт функціонального класу використовується так, як якщо б він був

функцією.

У другому операторі виведення вираз 1f_greater () використовується для виклику

конструктора за замовчуванням класу if_greater. Результатом виконання цього

виразу є об'єкт класу 1f_greater. Далі, як і в попередньому випадку, для цього

об'єкту викликається функція з двома аргументами, записаними в круглих дужках.

Можна визначити перевантажені операції виклику функції з різною кількістю

аргументів.

Функціональні об'єкти широко застосовуються у стандартній бібліотеці C + +.

Перевантаження операції індексування

Операція індексування [] зазвичай перевантажується, коли тип класу представляє

безліч значень, для якого індексування має сенс. Операція індексування повинна

повертати посилання на елемент, що міститься в множині. Покажемо це на

прикладі класу Vect, призначеного для зберігання масиву цілих чисел і безпечної

роботи з ним:

# 1nclucle <iostream.h>

# 1nclude <stdlib.h>

class Vect {

public:

explicit Vect (int n = 10):

VectCconst int a []. ініціалізація масивом

-VectO {delete [] p:}

int & operator [] (int i):

void PrintO:

Page 111: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

111

private:

int * p:

int size:

}:

Vect :: Vect (int n): size (n) {

p = new int [size]:

}

Vect :: Vect (const int a []. Int n): size (n) {

p = new intCsize]:

for (int i = 0: i <size: i + +) p [i] = a [i]:

}

Перевантаження операції індексування:

int & Vect :: operator [] (int i) {

if (i <0 II i> = size) {

cout «" Невірний індекс (i = "« i «

")" «Endl:

cout «" Завершення програми "« endl:

exit (O):

}

return p [i]:

}

void Vect :: Print () {

for (int i = 0: i <size; i + +)

cout «p [i]« "";

cout «endl:

}

int ma1n () {

int АГГ [10] = {1. 2. 3. 4. 5. 6. 7. 8. 9. 10};

Vect а (АГГ, 10);

Page 112: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

112

a.PrintO;

cout «a [5]« end!;

cout «a [12]« endl;

return 0;

}

Результат роботи програми:

123456789 10

6

Невірний індекс (1 = 12)

Завершення програми

Перевантажена операція індексування отримує цілий аргумент і перевіряє, чи

лежить його значення в межах діапазону масиву. Якщо так, то повертається адреса

елемента, що відповідає семантиці стандартної операції індексування.

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

explicit для того, щоб він не був конструктором перетворення типу, що

викликається неявно. Ключове слово explicit вказує на те, що цей конструктор

буде викликатися тільки явним чином.

Операцію [] можна визначати лише як метод класу.

Вказівники на елементи класів

До елементів класів можна звертатися за допомогою вказівників. Для цього

визначені операції. * І -> *. Вказівники на поля і методи класу визначаються по-

різному.

Формат вказівника на метод класу:

возвр ^ тип (ім'я ^ класу :: * імя_вказівника) (параметри);

Наприклад, опис вказівника на методи класу monstr

int get__health () {return health;}

int get ^ ammoO {return ammo:}

(A також на інші методи цього класу з такою ж сигнатурою) буде мати вигляд:

int (monstr :: * pget) ():

Такий вказівник можна задавати як параметр функції. Це дає можливість

передавати в функцію ім'я методу:

void fun (int (monstr :: * pget) ()) {

Page 113: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

113

(* This. * Pget) (): / / Виклик функції через операцію. *

(This-> * pget) (): / / Виклик функції через операцію -> *

}

Можна налаштувати вказівник на конкретний метод за допомогою операції

взяття адреси:

Присвоєння значення вказівником:

pget «& monstr :: get_health;

monstr Vasia. * P;

p = new monstr;

Виклик функції через операцію. *:

int Vasin ^ health - (Vasia. * pget) ();

Виклик функції через операцію -> *:

int p__heaUh = (p-> * pget) ();

Вказівнику на метод можна привласнювати тільки адреси методів, які мають

відповідний заголовок.

Не можна визначити вказівник на статичний метод класу.

Не можна перетворити вказівник па метод в вказівник на звичайну функцію, яка

не є елементом класу.

Як і вказівники на звичайні функції, вказівники на методи використовуються в

тому випадку, коли виникає необхідність викликати метод, ім'я якого невідоме.

Однак на відміну вказівника на змінну або звичайну функцію, вказівник на метод

не посилається на певну адресу пам'яті. Він більше схожий на індекс у масиві,

оскільки задає зсув. Конкретна адреса в пам'яті виходить шляхом поєднання

вказівника на метод з вказівником на певний об'єкт.

ПРИМІТКА

Методи, що викликаються через вказівники, можуть бути віртуальними. При

цьому викликається метод, відповідний типу об'єкта, до якого застосовувався

вказівник.

Формат вказівника на поле класу:

тип_даних (іня_класса :: * імя_вказівника):

У визначення вказівника можна включити його ініціалізацію у формі:

Page 114: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

114

Ім'я_класу :: імя__поля ;/ / Поле повинно бути public

Якби поле health було оголошено як public, визначення вказівника на нього мало

б вигляд:

int (monstr :: * phealth) »& nranstr :: health:

cout «Vasia. * phealth: / / Звернення через операцію. *

cout «p-> * phealth: / / Звернення через операцію -> *

Зверніть увагу на те, що вказівники на поля класів не є звичайними вказівниками

- адже при присвоєнні їм значень вони не посилаються на конкретну адресу

пам'яті, оскільки пам'ять виділяється не під класи, а під об'єкти класів.

Рекомендації щодо складу класу

Як правило, клас як тип, визначений користувачем, повинен містити приховані

(private) поля і наступні функції:

а конструктори, що визначають, як ініціалізувалися об'єкти класу;

набір методів, що реалізують властивості класу (при цьому методи, які

повертають значення прихованих полів класу, описуються з модифікатором const,

що вказує, що вони не повинні змінювати значення полів);

набір операцій, що дозволяють копіювати, привласнювати, порівнювати об'єкти і

проводити з ними інші дії, що вимагаються по суті класу;

клас винятків, використовуваний для повідомлень про помилки за допомогою

генерації виняткових ситуацій (про це буде розказано).

Якщо існують функції, які працюють з класом чи декількома класами через

інтерфейс (тобто доступ до прихованих полям їм не потрібно), можна описати їх

поза класів, щоб не перевантажувати інтерфейси, а для забезпечення логічного

зв'язку помістити їх у спільне з цими класами простір імен , наприклад:

namespace Staff {

class monstr {/ * ... * /}:

class hero {/ * ... * /};

void interact (hero, monstr):

Page 115: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

Змістовий модуль 6. Розширені можливості С++.

Основні розділи теми.

1. Вбудовані функції

2. Використання ключового слова inline

3. Сенс вбудованих функцій

4. Вбудовані функції і класи

5. Використання операторів мови Асемблер

6. Механізм обробки виняткових ситуацій

7. Тонкості обробки виняткових ситуацій

8. Стандартні виняткові ситуації.

Питання для самоперевірки.

1. Сенс використання вбудованих функцій

2. Як використовувати ключове слова inline

3. Вбудовані функції і класи

4. Як і для чого використовуються оператори мови Асемблер

5. Що називається винятковою ситуацією?

6. Що називається обробкою виняткової ситуації?

7. Як здійснюється обробка виняткової ситуації?

8. Опишіть зміст ключових слів try, catch і throw.

9. Опишіть особливості блоку try.

10. Опишіть особливості блоку catch.

11. Опишіть особливості вживання ключового слова throw.

12. Опишіть ієрархію виняткових ситуацій.

13. Як обробляються непередбачувані ситуації?

14. Як задати нові обробники непередбачуваних ситуацій?

15. Назвіть і опишіть стандартні виняткові ситуації.

Вбудовані функції і асемблерні коди

Ваші програми інтенсивно використовували функції. Як ви вже знаєте,

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

збільшують витрати (збільшують час виконання), поміщаючи параметри в стек

при кожному виклику. З даної теорії ви дізнаєтеся, що для коротких функцій

можна використовувати метод, званий вбудованим кодом, який поміщає

оператори функції для кожного її виклику прямо в програму, уникаючи таким

чином витрат на виклик функції. Використовуючи вбудовані (inline) функції,

ваші програми будуть виконуватися трохи швидше. До кінця цієї лабораторної

роботи ви освоїте наступні основні концепції:

Для поліпшення продуктивності за рахунок зменшення витрат на

виклик функції ви можете змусити компілятор C ++ вбудувати в

програму код функції, подібно до того, як це робиться при заміщенні

макрокоманд.

Page 116: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

Використовуючи вбудовані (inline) функції, ваші програми

залишаються зручними для читання (читає програму бачить виклик

функції), але ви уникаєте витрат на виклик функції, які викликані

розташуванням параметрів в стек і їх подальшим витяганням з стека, а

також переходом до тіла функції і подальшим поверненням з неї.

Залежно від вимог, що пред'являються до вашій програмі, іноді вам

буде потрібно використовувати мову асемблера для вирішення певної

задачі.

Для спрощення застосування програмування на мові асемблера C ++

дозволяє визначити функції на вбудованій мові асемблера всередині

ваших програм на C++.

Вбудовані функції

Коли ви визначаєте в своїй програмі функцію, компілятор C ++ переводить

код функції в машинну мову, зберігаючи тільки одну копію інструкцій функції

всередині вашої програми. Кожен раз, коли ваша програма викликає функцію,

компілятор C ++ поміщає в програму спеціальні інструкції, які заносять

параметри функції в стек і потім виконують перехід до інструкцій цієї функції.

Коли оператори функції завершуються, виконання програми триває з першого

оператора, який слідує за викликом функції. Поміщення аргументів в стек і

перехід в функцію і з неї вносить витрати, через які ваша програма виконується

трохи повільніше, ніж якби вона розміщувала ті ж оператори прямо всередині

програми при кожному посиланні на функцію. Наприклад, припустимо, що

наступна програма CALLBEEP.CPP викликає функцію show_message, яка

вказане число раз видає сигнал на динамік комп'ютера і потім виводить

повідомлення на дисплей:

#include <iostream.b>

void show_message(int count, char *message)

{

int i;

for (i = 0; i < count; i++) cout << ‘\a’;

cout << message << endl;

}

void main(void)

{

show_message(3, «Вчимося програмувати на мові C++»);

show_mes sage(2, «Лабораторна робота №27»);

}

Наступна програма NO_CALL.CPP не викликає функцію show_message.

Замість цього вона поміщає в собі ті ж оператори функції при кожному

посиланні на функцію:

#include <iostream.h>

void main (void)

{

int i;

for (i = 0; i < 3; i++) cout << ‘\a’;

cout << » Вчимося програмувати на мові C++» << endl;

Page 117: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

for (i = 0; i < 2; i++) cout << ‘\a’;

cout << «Лабораторна робота №30» << endl;

}

Обидві програми виконують одне і те ж. Оскільки програма NO_CALL не

викликає функцію show_message, вона виконується трохи швидше, ніж програма

CALLBEEP. В даному випадку різницю в часі виконання визначити неможливо,

але, якщо у звичайній ситуації функція буде викликатися 1000 разів, ви, мабуть,

помітите невелике збільшення продуктивності. Однак програма NO_CALL більш

заплутана, ніж її двійник CALL_BEEP, отже, більш важка для сприйняття.

При створенні програми Ви завжди повинні спробувати визначити, коли

краще використовувати звичайні функції, а коли краще скористатися

вбудованими функціями. Для більш простих програм переважно

використовувати звичайні функції. Однак, якщо ви створюєте програму, для якої

продуктивність має першорядне значення, вам слід було б зменшити кількість

викликів функцій. Один із способів зменшення кількості викликів функцій

полягає в тому, щоб помістити відповідні оператори прямо в програму, як тільки

що було зроблено в програмі NO_CALL. Однак, як ви могли переконатися,

заміна тільки однієї функції внесла значну плутанину в програму. На щастя, C

++ надає ключове слово inline, яке забезпечує кращий спосіб.

Використання ключового слова inline

При оголошенні функції всередині програми C ++ дозволяє вам

випередити ім'я функції ключовим словом inline. Якщо компілятор C ++

зустрічає ключове слово inline, він поміщає в здійсненний файл (машинну мову)

оператори цієї функції в місці кожного її виклику. Таким чином, можна

поліпшити читаність ваших програм на C ++, використовуючи функції, і в той

же час збільшити продуктивність, уникаючи витрат на виклик функцій.

Наступна програма INLINE.CPP визначає функції mах і min як inline:

#include <iostream.h>

inline int max(int a, int b)

{

if (a > b) return(a);

else return(b) ;

}

inline int min(int a, int b)

{

if (a < b) return(a);

else return(b);

}

void main(void)

{

cout << «Мінімум з 1001 і 2002 дорівнює» << min(1001, 2002) << endl;

cout << «Максимум з 1001 і 2002 дорівнює» << max(1001, 2002) << endl;

}

В даному випадку компілятор C ++ замінить кожен виклик функції на

відповідні оператори функції. Продуктивність програми збільшується без її

ускладнення.

Page 118: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

Про вбудовані функції

Якщо компілятор C ++ перед визначенням функції зустрічає ключове

слово inline, він буде замінювати звернення до цієї функції (виклики) на

послідовність операторів, еквівалентну виконання функції. Таким чином ваші

програми покращують продуктивність, позбавляючись від витрат на виклик

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

функцій.

Вбудовані функції і класи

Як ви вже знаєте, при визначенні класу ви визначаєте функції цього класу

всередині або поза класом. Наприклад, клас employee визначає свої функції

всередині самого класу:

class employee

{

public:

employee(char *name, char *position, float salary)

{

strcpy(employee::name, name);

strcpy(employee::position, position);

employee::salary = salary;

}

void show_employee(void)

{

cout << «Ім’я: » << name << endl;

cout << «Посада: » << position << endl;

cout << «Оклад: $» << salary << endl;

}

private:

char name [64];

char position[64];

float salary;

};

Розміщуючи подібним чином функції всередині класу, ви тим самим

повідомляєте їх вбудованими {inline). Якщо ви створюєте вбудовані функції

класу цим способом, C ++ дублює функцію для кожного створюваного об'єкта

цього класу, поміщаючи вбудований код при кожному посиланні на метод

(функцію) класу. Перевага такого вбудованого коду полягає в збільшенні

продуктивності. Недоліком є дуже швидке збільшення обсягу самого визначення

класу. Крім того, включення коду функції в визначення класу може істотно

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

Для поліпшення читаності визначень ваших класів ви можете винести

функції з визначення класу, як ви зазвичай і робите, і розмістити ключове слово

inline перед визначенням функції. Наприклад, таке визначення змушує

компілятор використовувати вбудовані оператори для функції show_employee:

inline void employee::show_employee(void)

{

cout << «Ім’я: » << name << endl;

Page 119: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

cout << «Посада: » << position << endl;

cout << «Оклад: $» << salary << endl;

}

Використання операторів мови Асемблер

Як ви знаєте, програмісти можуть створювати програми, використовуючи

широкий спектр мов програмування. Потім компілятор перетворює оператори

програми в машинний код (нулі і одиниці), який розуміє комп'ютер. Кожен тип

комп'ютерів підтримує проміжний мову, званий мовою асемблера, яка потрапляє

в категорію між машинною мовою і мовою програмування, такою як C ++.

Мова асемблера використовує інші символи для подання інструкцій

машинної мови. Залежно від призначення ваших програм, можливо, вам буде

потрібно виконати операції низького рівня, для яких необхідно використовувати

оператори мови асемблера. У таких випадках ви можете використовувати

оператор C ++ asm для вбудовування операторів мови асемблера в програму.

Більшість створюваних вами програм не зажадають операторів мови асемблера.

Наступна програма USE_ASM.CPP використовує оператор asm, щоб вставити

оператори мови асемблера, необхідні для озвучування динаміка комп'ютера в

середовищі MS-DOS:

#include <iostream.h>

void main(void)

{

cout << «Зараз буде дзвонити!» << endl;

asm

{

MOV AH,2

MOV DL,7

INT 21H

}

cout << «Є!» << endl;

}

Як бачите, використовуючи оператор asm, програма комбінує C ++ і

оператори мови асемблера.

Вбудовані функції покращують продуктивність ваших програм,

зменшуючи витрати на виклик функцій. З цієї лабораторної роботи ви дізналися,

як і де використовувати вбудовані функції в своїх програмах. Ви також

дізналися, що іноді вашим програмам необхідно використовувати мову

асемблера для вирішення певних завдань. Переконайтеся, що ви освоїли такі

основні концепції:

Поміщення параметрів в стек і перенесення до елемента і з неї

вносить витрати, через які ваша програма виконується трохи

повільніше.

Ключове слово inline змушує компілятор C ++ замінювати виклик

функції еквівалентної послідовністю операторів, які б виконувала цю

функцію. Оскільки вбудовані оператори позбавляють від витрат на

виклик функції, програма буде виконуватися швидше.

Page 120: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

Якщо ви використовуєте вбудовані функції всередині класу, кожен

створюваний вами об'єкт використовує свої власні вбудовані оператори.

Зазвичай всі об'єкти одного і того ж класу спільно використовують один

і той же код функції.

Ключове слово asm дозволяє вам вбудовувати оператори мови

асемблера в програми на C ++.

Дослідження виняткових ситуацій С ++ для обробки помилок.

Одним з найбільш яскравих утілень принципу об'єктно-орієнтованого

програмування є механізм обробки виняткових ситуацій у мові С++. У ході

виконання програми можуть виявитися різні помилки. Вони можуть бути

позв'язані з неправильним програмуванням (наприклад, вихід індексу масиву за

межі припустимого чи переповнення пам'яті), а іноді їхня причина не залежить

від програміста (скажемо, розрив зв'язку при мережевому з'єднанні). У кожній з

цих ситуацій реакція програми непередбачена. Іноді вона завершує виконання, і

лише після закінчення деякого інтервалу часу починають позначатися наслідку

помилки, а частіше програма негайно припиняє роботу, піддаючи ризику дані,

що знаходяться в пам'яті чи у файлі. Якщо не передбачити акуратне завершення

роботи, використовуючи обробку виняткових ситуацій, результати можуть

виявитися неприємними.

В подальшому ми будемо називати винятковою ситуацією будь-яку подію,

що вимагає особливої обробки. При цьому зовсім неважливо, чи є ця подія

фатальною чи простою помилкою. Перевірка умов, що описують виняткову

ситуацію, і реакція на її виникнення називається обробкою виняткової ситуації.

Ця задача покладається на оброблювача виняткової ситуації.

Механізм обробки виняткових ситуацій

Обробка виняткових ситуацій у мові С++ є об’єктно-орієнтованою. Це

значить, що виняткова ситуація є об'єктом, що генерується при виникненні

незвичайних умов, передбачених програмістом, і передається оброблювачу, що

неї перехоплює. Об'єктом, що описує природу виняткової ситуації, може бути

будь-як сутність — літерал, рядок, об'єкт класу, число і т.д. Не слід думати, що

виняткова ситуація обов'язково повинна бути об'єктом якого-небудь класу.

Обробка виняткових ситуацій

В основі обробки виняткових ситуацій у мові С++ лежать три ключових слова:

try, catch і throw. Якщо програміст підозрює, що визначений фрагмент програми

може спровокувати помилку, він повинний занурити цю частину коду в блок try.

Необхідно мати на увазі, що зміст помилки (за винятком стандартних ситуацій)

визначає сам програміст. Це значить, що програміст може задати будь-яку

умову, що приведе до створення виняткової ситуації. Після цього необхідно

вказати, у яких умовах варто генерувати виняткову ситуацію. Для цієї мети

призначене ключове слово throw. І нарешті, виняткову ситуацію потрібно

перехопити й обробити в блоці catch. Ось як виглядає ця конструкція.

try

{

// Тіло блоку try

Page 121: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

if(умова)throw виняткова_ситуація

}

catch(тип1 аргумент)

{

// Тіло блоку catch

}

catch(тип2 аргумент)

{

// Тіло блоку catch

}

.

.

.

catch(типN аргумент)

{

// Тіло блоку catch

}

Розмір блоку try не обмежений. У нього можна занурити як один оператор, так і

цілу програму. Один блок try можна зв'язати з довільною кількістю блоків catch.

Оскільки кожен блок catch відповідає окремому типу виняткової ситуації,

програма сама визначить, який з них виконати. У цьому випадку інші блоки catch

не виконуються. Кожен блок catch має аргумент, що приймає визначене

значення. Цей аргумент може бути об'єктом будь-якого типу. Якщо програма

виконана правильно й у блоці try не виникло жодної виняткової ситуації, усі

блоки catch будуть зігноровані. Якщо в програмі виникла подія, що програміст

вважає небажаним, оператор throw генерує виняткову ситуацію. Для цього

оператор throw повинний знаходитися усередині блоку try або усередині функції,

викликуваної усередині блоку try.

Якщо в програмі виникла виняткова ситуація, для якої не передбачені

перехоплення й обробка, викликається стандартна функція terminate(), що, у

свою чергу, викликає функцію abort(). Утім, іноді виняткова ситуація не є

небезпечної. У цьому випадку можна виправити помилку (наприклад,

привласнити нульовому знаменнику ненульове значення) і продовжити

виконання програми.

Розглянемо найпростіший приклад.

#include <iostream>

using namespace std;

int main()

{

int n = 10, m = 0;

printf("Початок\n");

try

{

printf("У блоці try\n");

Page 122: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

if(m==0) throw "Divide by zero"; else n=n/m;

printf("Подальша частина блоку не

виконується!");

}

catch (const char* s)

{

printf("%s\n",s);

}

printf("Кінець\n");

return 0;

}

Ця програма виводить на екран наступні рядки.

Начало

Усередині блоку try

Ділення на нуль

Кінець

Простежимо за потоком керування при виконанні цієї програми. Спочатку

з'являються і ініціалізуються дві целочисельні перемінні (одна з них дорівнює

нулю). Потім виводиться повідомлення про початок виконання програми, і потік

керування входить у блок try. Після виводу рядка повідомлення про вхід у блок

try, потік керування переходить до перевірки рівності m==0. Оскільки ця рівність

є істиною, генерується виняткова ситуація (у даному випадку — константний

рядок). Керування негайно передається блоку catch, аргументом якого є

константний символьний вказівник, ігноруючи всі інші оператори в блоці try. У

цій програмі блок catch не робить жодних спроб виправити помилку. Замість

цього він просто видає повідомлення — рядок, отриманий як аргумент — і

передає керування оператору, що слідує за блоком. На закінчення функція

printf() виводить на екран рядок Кінець, і програма завершує свою роботу.

Тип виняткової ситуації повинний збігатися з типом аргументу розділу catch.

Поглянемо, что відбудеться, якщо цією умовою зневажити.

#include <iostream> using namespace std;

int main()

{

int n = 10, m = 0;

printf("Начало\n");

try

{

printf("У блоці try\n");

if(m==0) throw "Розподіл на нуль"; else n=n/m;

printf("Подальша частина блоку не виконується!");

}

catch (const char s) // Помилка! Необходимо const char* s!

{

printf("%s\n",s);

Page 123: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

}

printf("Кінець\n");

return 0;

}

У цій програмі ми зробили цілком “ природну” помилку — забули поставити

зірочку в оголошенні аргументу. Тепер блок catch очікує виняткову ситуацію, що

представляє собою константний символ, а не вказівник. Ця помилка приводить

до аварійного завершення роботи програми.

Покажемо, що відбудеться, якщо виняткова ситуація генерується усередині

функції, яка викликається в блоці try.

Виняткова ситуація, згенерована усередині функції

#include <iostream>

using namespace std;

int Denominator(int);

int main()

{

int n = 10, m;

printf("Початок\n");

try

{

printf("У блоці try\n");

m=Denominator(0);

printf("Подальша частина блоку не виконується!");

}

catch (const char* s)

{

printf("%s\n",s);

}

printf("Кінець\n");

return 0;

}

int Denominator(int i)

{

if(i==0)throw "Ділення на нуль";

return i;

}

У цій програмі виняткова ситуація генерується у функції Denominator(), яка

викликається в блоці try. Завдяки цьому результати роботи програми цілком

збігаються з попередніми.

Якщо блок try знаходиться усередині функції, обробка виняткової ситуації

виконується при кожнім виклику.

Розміщення блоку try усередині функції

Page 124: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

#include <iostream>

using namespace std;

int Denominator(int);

int main()

{

int n = 10, m;

printf("Початок\n");

m=Denominator(0);

n = enominator(11);

printf("Кінець\n");

return 0;

}

int Denominator(int i)

{

printf("У функції Denominator\n");

try

{

printf("У блоці try\n");

if(i==0) throw("Ділення на нуль!");

if(i>10) throw 10;

printf("Подальша частина блоку не виконується!");

}

catch (const char* s)

{

printf("%s\n",s);

}

catch (int n)

{

printf("Чисельник більше %d\n",n);

}

return i;

}

На екрані з'являться наступні рядки.

Усередині функції Denominator

У блоці try

Ділення на нуль!

Усередині функції Denominator

У блоці try

Чисельник більше 10 Кінець

У цій програмі передбачене перехоплення двох виняткових ситуацій. Перша з

них має тип const char* і генерується, коли знаменник дорівнює нулю, а друга —

тип int і генерується, коли чисельник перевищує 10. Як бачимо, ці виняткові

ситуації перевіряються і перехоплюються при кожнім виклику функції

Denominator().

Page 125: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

Розглянемо тепер приклад, у якому функція Denominator() лише генерує

виняткові ситуації, а їх обробка здійснюється у функції main().

Окрема обробка виняткових ситуацій

#include <iostream>

using namespace std;

int Denominator(int);

int main()

{

int n = 10, m;

printf("Початок\n");

try

{

printf("У блоці try\n");

m=Denominator(0);

n = Denominator(11);

printf("Подальша частина блоку не виконується!");

}

catch (const char* s)

{

printf("%s\n",s);

}

catch (int n)

{

printf("Чисельник більше 10 %d\n",n);

}

printf("Кінець\n");

return 0;

}

int Denominator(int i)

{

printf("Усередині функції Denominator\n");

if(i==0) throw("Розподіл на нуль!"); if(i>10)

throw 10;

printf("Кінець функції Denominator\n");

return i;

}

Результат демонструє декілька важливих особливостей, властивим функціям,

що збуджують, але не обробляють виняткову ситуацію.

Початок

Усередині блоку try

Усередині функції

Denominator Розподіл на

нуль!

Кінець

Page 126: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

По-перше, блоки try і catch нерозривні. Не можна помістити блок try у

функцію, залишивши блок catch у функції main(). Необхідно або обробити

виняткову ситуацію усередині функції, як це зроблено в більш ранньому

прикладі, або перенести обробку в модуль виклику. У першому випадку функція,

завершивши обробку, повертає визначене її специфікацією значення, а в другому

— виняткову ситуацію. Таким чином, можна обійти обмеження мови С++,

відповідно до якого функція може повертати лише одне значення, тип якого

визначений заздалегідь. По-друге, механізм обробки виняткових ситуацій

дозволяє створювати альтернативні значення, що повертаються. По-третє,

функції можуть генерувати декілька виняткових ситуацій. Збудивши одну з них,

вони негайно припиняють своє виконання і повертають виняткову ситуацію в

модуль виклику. Необхідно враховувати, що присвоювання m=Denominator(0) чи

n=Denominator(11) у цьому випадку не виконуються.

Представимо тепер ланцюжок викликів функцій.

Ланцюгове генерування виняткових ситуацій: перший варіант

#include <stdio.h>

using namespace std;

int Check(int);

int Divide(int, int);

int main()

{

int n = 10, m=0, l;

printf("Початок\n"); l = Divide(n,m);

printf("Кінець\n");

return 0;

}

int Check(int i)

{

printf("Усередині функції Check\n");

if(i==0) throw("Розподіл на нуль усередині функції Check!");

printf("Кінець функції Check\n");

return i;

}

int Divide(int n, int m)

{

printf("Усередині функції Divide\n");

try{m=Check(m);}

catch (const char* s)

{

printf("%s\n",s);

return 1;

}

printf("Кінець функції Divide\n");

return n/m;

}

Page 127: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

Простежимо за передачею виняткової ситуації.

Початок

Усередині функції Divide

Усередині блоку try

Усередині функції Check

Ділення на нуль усередині функції Check!

Кінець

При виклику функції Divide() перевіряється знаменник m. Для цього

викликається функція Check(). Якщо знаменник дорівнює нулю, усередині цієї

функції генерується виняткова ситуація, що має тип const char*. Обробка цієї

виняткової ситуації усередині функції Check() не передбачена, тому вона

передається нагору по ланцюжку викликів — функції Divide(). Потім керування

передається функції main(), і виконання програми завершується.

Перенесемо обробку виняткової ситуації у функцію main().

Ланцюгове генерування виняткових ситуацій: другий варіант

#include <iostream>

using namespace std;

int Check(int);

int Divide(int, int);

int main()

{

int n = 10, m=0, l;

printf("Початок\n");

try

{

printf("Усередині блоку try\n"); l = Divide(n,m);

}

catch (const char* s)

{

printf(" %s\n",s);

}

printf("Кінець\n");

return 0;

}

int Check(int i)

{

printf("Усередині функції Check\n");

if(i==0) throw("Розподіл на нуль усередині функції main!");

printf("Кінець функції Check\n");

return i;

}

int Divide(int n, int m)

{

Page 128: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

printf("Усередині функції Divide\n");

m=Check(m);

printf("Кінець функції Divide\n");

return n/m;

}

Результат роботи цієї функції такий.

Початок

Усередині блоку try

Усередині функції Divide

Усередині функції Check

Ділення на нуль усередині функції main!

Кінець

Оскільки усередині функцій Check() і Divide() обробка виняткової ситуації не

передбачена, вона передається в головний модуль, про що свідчить представлена

нижче рядок.

Ділення на нуль усередині функції main!

Перехоплення класів виняткових ситуацій

Як відзначено вище, виняткова ситуація — це об'єкт, що може мати будь-який

тип, як убудований, так і користувальницький. Створення класів

користувальницьких ситуацій дозволяє програмісту точніше визначати реакцію

програми на небажану ситуацію. Розглянемо приклад, що ілюструє ця теза.

#include <iostream>

using namespace std;

class Error

{

public: int m;

Error(){ printf("Помилка\n");}

Error(int x):m(x){}

void Message(){ printf("Розподіл на нуль!");}

};

class Rational

{

int n; int m;

public:

Rational(int x, int y){ n = x; if(y==0)throw Error(y); else n = y; }

~Rational(){printf("Dtor Rational");}

};

int main()

{

try

{

Rational q(1,0);

Page 129: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

}

catch(Error& zero)

{

zero.Message();

return -1;

}

return 0;

}

Клас Rational реалізує концепцію раціонального числа. Зрозуміло, необхідно

заборонити створення раціональних чисел, у яких знаменник дорівнює нулю.

Для цього передбачений клас Error, що представляє собою виняткову ситуацію.

Його об'єкт створюється тільки в тому випадку, якщо поле m в об'єкті класу

Rational дорівнює нулю.

В цьому прикладі почата спроба створити об'єкт виду 1/0. Конструктор класу

Rational згенерував виняткову ситуацію класу Error, що була по посиланню

передане в розділ catch. У цьому розділі відбувається звертання до функції

Message() — члену класу Error. У підсумку, на екрані з'явиться таке

повідомлення.

Ділення на нуль!

Зверніть увагу на те, що виняткова ситуація передається по посиланню. Це

зовсім не обов’’ язково, але дуже бажано, оскільки передача по посиланню

ефективніше, ніж передача за значенням.

Ієрархія виняткових ситуацій

Оскільки виняткова ситуація може бути об'єктом класу, у мові С++ існує

можливість створювати ієрархію виняткових ситуацій. У цьому випадку блок

catch перехоплює об'єкти не тільки базового, але і похідних класів. При генерації

похідних виняткових ситуацій це приводить до непорозумінь — їх перехоплює

блок catch, призначений для обробки базових виняткових ситуацій.

Створення ієрархії виняткових ситуацій

#include <iostream>

using namespace std;

class ErrorBase

{

public: long m;

ErrorBase(){ printf("ErrorBase\n");}

ErrorBase(long x):m(x){printf("ErrorBase\n");}

void Message(){ printf("Розподіл на нуль!");}

};

class ErrorDerived:public ErrorBase

{

public: long m;

ErrorDerived(){ printf("ErrorDerived\n");}

ErrorDerived(long x):m(x){printf("ErrorDerived\n");} void

Message(){ printf("Розподіл на нескінченність!");}

};

Page 130: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

class Rational

{

long n; long m;

public:

Rational(long x, long y)

{

n = x;

if(y==0)throw ErrorBase(y);

else n = y;

if(y>=1000000)throw ErrorDerived(y);

else n = y;

}

~Rational(){printf("Dtor Rational");}

};

int main()

{

try

{

Rational q(1,1000000000);

}

catch(ErrorBase& zero)

{

zero.Message();

return -1;

}

catch(ErrorDerived& infinity)

{

infinity.Message();

return -1;

}

return 0;

}

У цій програмі оголошена ієрархія виняткових ситуацій — базовий клас

ErrorBase і похідний від нього клас ErrorDerived. Виняткові ситуації базового

класу генеруються, якщо конструктор класу Rational намагається створити об'єкт

із нульовим знаменником, а похідний клас ErrorDerived описує реакцію

програми, коли знаменник занадто великий (більше мільйона). Програма

виводить на екран наступні рядки.

ErrorBase

ErrorDerived

Розподіл на нуль!

Page 131: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

Як бачимо, при спробі створити об'єкт, знаменник якого більше мільйона, була

згенерована ситуація ErrorDivide, однак її перехопив блок catch, набудований на

базовий клас ErrorDivide.

Для розв’язку цієї проблеми необхідно розмістити блок catch, що відповідає

похідному класу, вище блоку, призначеного для перехоплення об'єктів класу

ErrorBase.

Перехоплення виняткових ситуацій визначеного класу

#include <iostream>

using namespace std;

class ErrorBase

{

public: long m;

ErrorBase(){ printf("ErrorBase\n");}

ErrorBase(long x):m(x){printf("ErrorBase\n");}

virtual void Message(){ printf("Розподіл на нуль!");}

};

class ErrorDerived:public ErrorBase

{

public: long m;

ErrorDerived(){ printf("ErrorDerived\n");}

ErrorDerived(long x):m(x){printf("ErrorDerived\n");} void

Message(){ printf("Розподіл на нескінченність!");}

};

class Rational

{

long n;

long m;

public:

Rational(long x, long y)

{

n = x;

if(y==0)throw ErrorBase(y);

else n = y;

if(y>=1000000)throw ErrorDerived(y);

else n = y;

}

~Rational(){printf("Dtor Rational");}

};

int main()

{

try

{

Rational q(1,1000000000);

Page 132: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

}

catch(ErrorDerived& infinity)

{

infinity.Message();

return -1;

}

catch(ErrorBase& zero)

{

zero.Message();

return -1;

}

return 0;

}

На екрані ми побачимо наступні рядки.

ErrorBase

ErrorDerived

Розподіл на нуль!

Зауважимо, що ця задача має ще одне розв’язок — можна оголосити функцію-

член Message() віртуальної. У цьому випадку оператор catch, призначений для

обробки виняткових ситуацій базового типу, буде як і раніше перехоплювати

об'єкти похідного типу, вважаючи їх базовими. Однак механізм заміщення

віртуальних функцій-членів базового класу дозволяє правильно обробити

виняткову ситуацію. Утім, цей спосіб вимагає визначеної обережності — вся

обробка повинна бути передбачена у функціях-членах похідного класу. Не слід

забувати, що керування передається в блок catch, призначений для перехоплення

базових виняткових ситуацій!

Перехоплення виняткових ситуацій похідного класу

#include <iostream>

using namespace std;

class ErrorBase

{

public: long m;

ErrorBase(){ printf("ErrorBase\n");}

ErrorBase(long x):m(x){printf("ErrorBase\n");}

virtual void Message(){ printf("Розподіл на нуль!\n");}

};

class ErrorDerived:public ErrorBase

{

public: long m;

ErrorDerived(){ printf("ErrorDerived\n");}

ErrorDerived(long x):m(x){printf("ErrorDerived\n");}

void Message(){ printf("Розподіл на нескінченність!\n");}

};

class Rational

Page 133: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

{

long n; long m;

public:

Rational(long x, long y)

{

n = x;

if(y==0)throw ErrorBase(y);

else n =y;

if(y>=1000000)throw ErrorDerived(y);

else n =y;

}

~Rational(){printf("Dtor Rational");}

};

int main()

{

try

{

Rational q(1,1000000000);

}

catch(ErrorBase& zero)

{

zero.Message();

printf("Усередині блоку сatch для класу ErrorBase!\n");

return -1;

}

catch(ErrorDerived& infinity)

{

infinity.Message();

printf("Усередині блоку catch для класу ErrorDerived!\n");

return -1;

}

return 0;

}

Тепер програма виводить на екран очікуваний результат, хоча керування

передається блоку catch, призначеному для перехоплення базових ситуацій,

оскільки він розташований вище блоку, що відповідає похідної виняткової

ситуації.

ErrorBase

ErrorDerived

Розподіл на нуль!

Усередині блоку catch для класу ErrorBase!

Page 134: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

Зверніть увагу на те, що перехоплення виняткових ситуацій, що виникли в

конструкторі, припиняє створення об'єкта. З цієї причини деструктор наприкінці

програми не викликається.

Тонкості обробки виняткових ситуацій

Розглянемо декілька корисних прийомів, що дозволяють ефективно

використовувати механізм виняткових ситуацій.

Тотальне перехоплення виняткових ситуацій

Іноді ретельна деталізація виняткових ситуацій не потрібна. Наприклад, у

попередніх прикладах їх обробка проводилася майже однаково, за винятком

супутніх повідомлень про помилки. Отже, було б зручно, якби блок catch можна

було настроїти на будь-яку виняткову ситуацію. Зробити це дозволяє наступна

конструкція.

catch(...)

{

// Перехоплення усіх виняткових ситуацій

}

Трикрапка в дужках означає, що блок catch здатний перехопити й обробити

будь-яку виняткову ситуацію. Повернемося до попереднього прикладу і

замінимо блоки catch новою конструкцією.

Тотальне перехоплення виняткових ситуацій: перший варіант

#include <iostream>

using namespace std;

void Message();

class ErrorBase

{

public: long m;

ErrorBase(){ printf("ErrorBase\n");}

ErrorBase(long x):m(x){printf("ErrorBase\n");}

virtual void Message(){ printf("Розподіл на нуль!");}

};

class ErrorDerived:public ErrorBase

{

public: long

m;

ErrorDerived(){ printf("ErrorDerived\n");}

ErrorDerived(long x):m(x){printf("ErrorDerived\n");} void

Message(){ printf("Розподіл на нескінченність!!");}

};

class Rational

{

long n; long m;

public:

Rational(long x, long y)

{

n = x;

Page 135: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

if(y==0)throw ErrorBase(y);

else n = y;

if(y>=1000000)throw ErrorDerived(y);

else n = y;

}

~Rational(){printf("Dtor Rational");}

};

int main()

{

int n = 5;

for(int i = 1; i<=n; i++)

{

try

{

if(i%2) Rational q(1,0); else Rational q(1,100000000);

}

catch(...)

{

Message();

}

}

return 0;

}

void Message(){ printf("Виняткова ситуація!\n");}

Ця програма генерує різні виняткові ситуації: при непарних індексах циклу i

— базову виняткову ситуацію класу ErrorBase, а при парних — похідну

виняткову ситуацію класу ErrorDivide. Усі вони перехоплюються тим самим

блоком catch.

ErrorBase

Виняткова ситуація!

ErrorBase

ErrorDerived

Виняткова ситуація! ErrorBase

Виняткова ситуація!

ErrorBase ErrorDerived

Виняткова ситуація!

ErrorBase

Виняткова ситуація!

Крім того, якщо програміст сумнівається в тім, що він передбачив усі можливі

виняткові ситуації, після всіх розділів catch можна поставити розділ catch(...). У

цьому випадку оператор catch(...) використовується для підстрахування,

перехоплюючи виняткові ситуації, не передбачені програмістом. (Це нагадує

застосування метки default в операторі switch.)

Для ілюстрації цього прийому повернемося до попередньої програми.

Нагадаємо, що виняткові ситуації базового класу ErrorBase генеруються при

Page 136: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

спробі створити об'єкт, що імітує раціональне число з нульовим знаменником, а

похідний клас ErrorDerived описує ситуацію, коли знаменник більше мільйона.

Ясно, що ці не усі виняткові ситуації, що можуть виникнути. Наприклад,

обмежений обсяг комп'ютерної пам'яті не дозволяє працювати з числами типу

long, довжина яких перевищує максимальну. Звичайно, можна було б

передбачити і це, написавши клас ErrorLong. (І це було б найкращим

розв’язком!)

catch(const char* s)

{

printf("%s",s);

}

І все, щоб перестрахуватися, можна поставити на останнє місце в ланцюжку

блоків catch універсальний перехоплювач.

Тотальне перехоплення виняткових ситуацій: другий варіант

#include <iostream>

#include <limits.h>

using namespace std;

class ErrorBase

{

public: long m;

ErrorBase(){ printf("ErrorBase\n");}

ErrorBase(long x):m(x){printf("ErrorBase\n");}

virtual void Message(){ printf("Розподіл на нуль!\n");}

};

class ErrorDerived:public ErrorBase

{

public: long m;

ErrorDerived(){ printf("ErrorDerived\n");}

ErrorDerived(long x):m(x){printf("ErrorDerived\n");}

void Message(){ printf("Розподіл на нескінченність!\n");}

};

class Rational

{

long n; long m;

public:

Rational(long x, long y)

{

if(x >= LONG_MAX) throw "Занадто великий

чисельник"; if(y >= LONG_MAX) throw "Занадто

великий знаменник"; n = x;

if(y==0)throw ErrorBase(y);

else n = y;

if(y>=1000000)throw ErrorDerived(y);

else n = y;

}

Page 137: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

~Rational(){printf("Dtor Rational");}

};

int main()

{

try

{

Rational q(1,LONG_MAX);

}

catch(ErrorBase& zero)

{

zero.Message();

printf("Усередині блоку catch для класу ErrorBase!\n");

return -1;

}

catch(ErrorDerived& infinity)

{

infinity.Message();

printf("Усередині блоку catch для класу ErrorDerived!\n");

return -1;

}

catch(...)

{

printf("Непередбачена виняткова ситуація!");

return -1;

}

return 0;

}

Оскільки блок catch(...) не має аргументів, у ньому важко передбачити точну

реакцію на виниклу виняткову ситуацію, наприклад вивести діагностичний

рядок. Мабуть, єдине, що залишається — припинити роботу програми і видати

на екран відповідне повідомлення .

Генерація виняткових ситуацій

Оголошуючи функцію, можна перелічити типи виняткових ситуацій, що їй

дозволено генерувати. Типи виняткових ситуацій, не включені в список, функція

генерувати не зможе. Спроба порушити це обмеження приведе до негайного

припинення роботи програми за допомогою ланцюжка викликів стандартних

функцій:

abort(). Якщо список порожній, функція взагалі не повинна генерувати

ніяких списку дозволених виняткових ситуацій використовується

наступна синтаксична

тип_ що повертається_значення ім'я_функції (аргументи) throw(виняткові

ситуації)

{

// ...

Page 138: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

}

Повернемося до демонстраційної програми, що використовує функцію

Check().

Список дозволених виняткових ситуацій: перший варіант

#include <iostream>

#define MAX

1000000 using

namespace std;

int CheckZero(int) throw (const char*); int

CheckMax(int) throw (int);

int Divide(int, int);

int main()

{

int n = MAX, m=0, l;

printf("Початок\n");

try

{

printf("Усередині блоку try\n"); l

= Divide(n+1,m);

}

catch (const char* s)

{

printf("%s\n",s);

}

catch (int k)

{

printf("Число чи більше дорівнює %d\n",k);

}

printf("Кінець\n");

return 0;

}

int CheckZero(int i) throw (const char*)

{

printf("Усередині функції Check\n");

if(i==0) throw("Розподіл на нуль усередині функції main!");

printf("Кінець функції Check\n");

return i;

}

int CheckMax(int i) throw (int)

{

printf("Усередині функції

CheckMax\n"); if(i>=MAX) throw MAX;

printf("Кінець функції CheckMax\n");

return i;

}

Page 139: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

int Divide(int n, int m)

{

printf("Усередині функції Divide\n");

n=CheckMax(n);

m=CheckMax(m);

m=CheckZero(m);

printf("Кінець функції Divide\n");

return n/m;

}

У цій програмі, що виконує розподіл двох цілих чисел, можливі наступні

виняткові ситуації: 1) якщо чисельник або знаменник перевищує максимально

припустиме число і 2) знаменник дорівнює нулю. Для порівняння числа з

максимальним використовується функція CheckMax(), а для порівняння з нулем

— функція CheckZero(). Функції CheckMax() дозволено генерувати виняткову

ситуацію типу int, а функція CheckZero() може генерувати виняткову ситуацію

типу const char*. Якщо ці функції спробують згенерувати виняткову ситуацію, не

зазначену в списку throw, виконання програми завершиться аварійно.

Початок

Усередині блоку try

Усередині функції Divide

Усередині функції

CheckMax Число більше

100000 Кінець

Припустимо тепер, що в програмі ані чисельник, ані знаменник не повинні

дорівнювати нулю. Отже, функція, що виконує перевірку, повинна мати

можливість генерувати обидві виняткові ситуації: число дорівнює нулю і число

більше максимального. Модифікований варіант програми виглядає так.

Список дозволених виняткових ситуацій: другий варіант

#include <iostream>

#define MAX

1000000 using

namespace std;

int Check(int) throw (const char*, int); int

Divide(int, int);

int main()

{

int n = 0, m=0, l;

printf("Початок\n");

try

{

printf("Усередині блоку try\n"); l

= Divide(n,m);

}

Page 140: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

catch (const char* s)

{

printf("%s\n",s);

}

catch (int k)

{

printf("Число чи більше дорівнює %d\n",k);

}

printf("Кінець\n");

return 0;

}

int Check(int i) throw (const char*, int)

{

printf("Усередині функції Check\n");

if(i==0) throw("Число дорівнює нулю!");

if(i>=MAX) throw MAX;

printf("Кінець функції Check\n");

return i;

}

int Divide(int n, int m)

{

printf("Усередині функції Divide\n");

n=Check(n);

m=Check(m);

printf("Кінець функції Divide\n");

return n/m;

}

Результат роботи цієї програми такий.

Початок

Усередині блоку try

Усередині функції

Divide Усередині

функції Check Число

дорівнює нулю Кінець

Список дозволених виняткових ситуацій поширюється лише на типи значень,

що повертаються функцією в модуль виклику. Інакше кажучи, ці обмеження не

стосуються блоків try, що знаходяться усередині функції. Якщо ми перенесемо

обробку виняткових ситуацій усередину функції Check(), їй можна взагалі

заборонити генерувати виняткові ситуації і передавати їх по ланцюжку модулів

виклику.

Заборона генерування виняткових ситуацій

#include <iostream>

#define MAX 100000

Page 141: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

using namespace std; int

Check(int) throw(); int

Divide(int, int);

int main()

{

int n = 0, m=MAX, l;

printf("Початок\n");

l = Divide(n,m+1);

printf("Кінець\n");

return 0;

}

int Check(int i) throw ()

{

printf("Усередині функції Check\n");

try

{

printf("Усередині блоку try\n"); if(i==0)

throw("Число дорівнює нулю!");

if(i>=MAX) throw MAX;

}

catch (const char* s)

{

return -1;

}

catch (int k)

{

printf("Число чи більше дорівнює %d\n",k);

return -1;

}

printf("Кінець функції Check\n");

return i;

}

int Divide(int n, int m)

{

printf("Усередині функції Divide\n");

n=Check(n);

m=Check(m);

if (n==-1 || m==-1) { printf("Аварійне завершення!"); return -1;}

printf("Кінець функції Divide\n");

return n/m;

}

Результат роботи цієї програми виглядає в такий спосіб.

Початок

Усередині функції

Divide Усередині

функції Check Усередині

Page 142: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

блоку try Число

дорівнює нулю!

Усередині функції Check

Усередині блоку try

Число чи більше дорівнює

100000 Аварійне завершення

Повторні виняткові ситуації

Іноді зручно обробляти виняткову ситуацію в декількох розділах catch, ніби

передаючи її по конвеєру. Уявимо собі, що на першому етапі оброблювач просто

констатує наявність проблеми і пропонує користувачу вирішити, що робити —

намагатися виправити чи помилку припинити роботу програми. Якщо

програміст вирішить продовжувати роботу, необхідно згенерувати ситуацію

знову й ужити заходів, що дозволяють врятувати положення. Якщо ж програміст

вирішить припинити роботу програми, викликається відповідна чи функція

виконується оператор return.

Передача виняткової ситуації по ланцюжку

#include <iostream>

#define MAX 100000

using namespace std;

int Check(int) throw(); int Divide(int, int);

int main()

{

int n = 0, m=MAX, l;

printf("Початок\n"); l = Divide(n,m+1);

printf("Кінець\n");

return 0;

}

int Check(int i) throw ()

{

int choice;

printf("Усередині функції Check\n");

try

{

printf("in try Усередині блоку try\n"); if(i==0) throw("Zero Число дорівнює

нулю!"); if(i>=MAX) throw MAX;

}

catch (const char* s)

{

printf("%s\n",s);

printf("Ваш вибір? 0 - Вихід; 1 - Продовжити"); scanf("%d",&choice);

if(!choice) { printf("Виходимо\n"); return -1;} else throw;

}

catch (int k)

{

Page 143: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

printf("Число чи більше дорівнює %d\n",k); return -1;

}

printf("Кінець функції Check\n"); return i;

}

int Divide(int n, int m)

{

printf("Усередині функції Divide\n"); try

{

n=Check(n);

}

catch (const char* s)

{

printf("%s\n",s); printf("Продовжуємо\n"); return -1;

}

try

{

m=Check(m);

}

catch (const char* s)

{

printf("%s\n",s); printf("Продовжуємо\n"); return -1;

}

if (n==-1 || m==-1)

{printf("Аварійне завершення!\n"); return -1;}

printf("Кінець функції Divide\n");

return n/m;

}

Проаналізуємо хід виконання цієї програми.

Початок

Усередині функції Divide

Усередині функції Check

Усередині блоку try

Число дорівнює нулю!

Ваш вибір? 0 - Вихід; 1 – Продовжити 1

Число дорівнює нулю!

Продовжуємо Кінець

Чисельник дробу дорівнює нулю, а знаменник — максимальному значенню.

Усередині функції Divide() викликається функція Check(), що повинна

перевірити коректність чисельника. Знайшовши, що чисельник дорівнює нулю,

функція Check() генерує виняткову ситуацію типу const char*. Її перехоплює

блок catch, усередині якого користувачу пропонується або вийти з програми,

увівши число 0, або продовжити виконання, увівши 1 чи будь-яке інше число. У

даному випадку користувач ввів одиницю. У відповідь на це функція Check()

повторно згенерувала виняткову ситуацію типу const char*. Для цього

Page 144: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

використовується оператор throw без параметрів, що генерує повторно поточну

ситуацію. Оскільки тому самому блоку try не можна поставити у відповідність

декілька блоків catch з однаковим набором аргументів, виняткова ситуація

передається по ланцюжку в модуль виклику. Про це свідчить рядок

Продовжуємо.

Аналогічна обробка виняткової ситуації передбачена і для знаменника. Якщо у

відповідь на запит програми користувач уведе нуль, на екрані з'являться наступні

рядки.

Виходимо

Усередині блоку

Check Усередині

блоку try

Число чи більше дорівнює

100000 Аварійне завершення!

Кінець

Непередбачені виняткові ситуації

Для того щоб реагувати на виняткові ситуації, обробка яких програмістом не

передбачена, у мові С++ використовуються функції terminate() і unexpected(),

оголошені в заголовку <exception>, а також функція abort(), прототип якої

міститься в заголовках <process.h> і <stdlib.h>.

Механізм роботи цих функцій виглядає в такий спосіб. Виняткова ситуація,

що не перехопив жодний оператор catch, передається нагору по ланцюжку

викликів. Якщо вона не обробляється в жодному з модулів виклику,

викликається terminate(), що за замовчуванням викликає функцію abort(). Такі

виняткові ситуації називаються непередбаченими (unexpected).

С допомогою стандартних засобів простежити, коли і які функції

викликаються, неможливо. Звичайно вважається, що спочатку викликається

функція unexpected(), що автоматично викликає функцію terminate(), а та у свою

чергу — функцію abort() за замовчуванням. На щастя, у мові С++ існує механізм

заміни функцій terminate() і unexpected() власними оброблювачами

непередбачених ситуацій. Для цього призначені функції set_terminate() і

set_unexpected(), специфікація яких міститься в заголовку <exception>.

new_unexpected set_unexpected(new_unexpected)

throw(); new_terminate set_terminate(new_terminate)

throw();

Ідентифікатори new_unexpected і new_terminate — імена нових оброблювачів.

Аргументами функцій set_unexpected() і set_terminate() є вказівники на ці

оброблювачі. Оскільки цими вказівниками автоматично є імена нових функцій,

синтаксична конструкція виглядає досить просто. Одержуючи адресу нового

оброблювача, функції set_unexpected() і set_terminate() повертають адреси старих

оброблювачів. Це дозволяє зберегти їх у пам'яті і відновити при необхідності

стару систему обробки непередбачених ситуацій. Заступники функцій

unexpected() і terminate() не повинні повертати керування програми. Вони

зобов'язані поводитися, як їхні прототипи.

Page 145: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

Продемонструємо систему обробки непередбачених ситуацій наступними

прикладами. Спочатку розглянемо програму, у якій не передбачена обробка

виняткової ситуації, що виникає, коли число перевищує максимально

припустиме.

Реакція на непередбачені виняткові ситуації

#include <iostream>

#define MAX 100000

using namespace std; int

Check(int) throw(); int

Divide(int, int);

int main()

{

int n = 0, m=MAX, l;

printf("Start\n"); l =

Divide(n,m+1);

printf("Finish\n");

return 0;

}

int Check(int i) throw ()

{

printf("In Check\n");

try

{

printf("In try\n"); if(i==0) throw("Zero!");

if(i>=MAX) throw MAX;

}

catch(const char* s)

{

printf("%s\n",s);

}

printf("End Check\n"); return i;

}

int Divide(int n, int m)

{

printf("In Divide Усередині функції Divide\n");

n=Check(n);

m=Check(m);

if (n==-1 || m==-1) { printf("Abnormal termination!\n"); return -1;}

printf("End Divide\n");

return n/m;

}

У цьому випадку на екрані з'являться такі рядки.

Початок

Усередині функції Divide

Усередині функції Check

Page 146: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

Усередині блоку try

Число дорівнює нулю! Кінець функції Check

Усередині функції Check

Усередині блоку try

На цьому виконання програми аварійно завершується. Справа в тім, що в ній

не передбачена реакція на виняткову ситуацію типу int. Пошук оброблювачів у

модулях виклику нічого не дав, і операційна система викликала функції

unexpected() і terminate(). Цікаво, а в якому порядку? Щоб відповісти на це

питання, замінимо стандартні функції unexpected() і terminate() своїми версіями.

Зверніть увагу на те, що знайти неопрацьовану виняткову ситуацію можна також

за допомогою функції uncaught_exception(), що повертає значення типу bool.

Застосування функції uncaught_exception()

#include <exception>

#include <iostream>

#include <process.h>

#define MAX 100000

using namespace std;

int Check(int) throw (const char*, int); int

Divide(int, int);

void new_unexpected();

void new_terminate();

int main()

{

int n = 0, m=MAX, l;

typedef void (*pNew_unexpected)();

typedef void (*pNew_terminate)();

pNew_unexpected oldAddress1;

pNew_terminate oldAddress2;

set_unexpected(new_unexpected);

set_terminate(new_terminate);

printf("Початок\n") l = Divide(n,m+1);

printf("Кінець\n");

oldAddress1 = set_unexpected(oldAddress1);

oldAddress2 = set_terminate(oldAddress2);

return 0;

}

int Check(int i) throw (const char*, int)

{

printf("Усередині функції Check\n");

try

{

printf("Усередині блоку try\n");

if(i==0) throw("Число дорівнює нулю!");

if(i>=MAX) throw MAX;

}

Page 147: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

catch(const char* s)

{

printf("%s\n",s);

}

printf("Кінець функції Check\n");

return i;

}

int Divide(int n, int m)

{

printf("Усередині функції Divide\n");

n=Check(n);

m=Check(m);

if (n==-1 || m==-1) { printf("Аварійне завершення!\n"); return -1;}

printf("Кінець функції Divide\n");

return n/m;

}

void new_unexpected()

{

printf("Новий оброблювач unexpected\n"); exit(-

1);

}

void new_terminate()

{

if(!uncaught_exception())

printf("Виявлена непередбачена виняткова ситуація!\n");

else printf("Ok!\n");

printf("Новий оброблювач terminate\n");

exit(-1);

}

Хід виконання програми ілюструється наступними рядками.

Початок

Усередині функції Divide

Усередині функції Check

Усередині блоку try

Число дорівнює нулю! Кінець

функції Check Усередині

функції Check Усередині

блоку try

Виявлена непередбачена виняткова ситуація! Новий оброблювач terminate

Як бачимо, при виявленні непередбаченої виняткової ситуації викликається

оброблювач new_terminate, що завершує роботу програми.

Стандартні виняткові ситуації

Page 148: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

У мові С++ передбачено вісім стандартних виняткових ситуацій, що

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

Виняткова ситуація Зміст Заголовок

Помилки при роботі з пам'яттю і типами

bad_alloc Помилка розподілу пам'яті <new>

bad_cast Некоректне приведення типів <typeinfo>

bad_typeid

Неправильне застосування

оператора typeid <typeinfo>

bad_exception

Непередбачена виняткова

ситуація <exception>

Логічні помилки

out_of_range

Вихід за межі припустимого

діапазону <stdexcept>

invalid_argument Невірний аргумент функції <stdexcept>

length_error

Перевищення припустимих

розмірів об'єкта <stdexcept>

Помилки при виконанні програми

domain_error

Вихід за межі припустимого

діапазону <stdexcept>

overflow_error Переповнення <stdexcept>

underflow_error Утрата значимості <stdexcept>

domain_error

Вихід за межі припустимого

діапазону <stdexcept>

ios_base::failure Помилка введення-висновку <stdexcept>

Ці класи утворять ієрархію, коренем якої є клас exception.

Частина з них зв'язана з класами, оголошеними в стандартній бібліотеці

шаблонів. Зупинимося поки на тих стандартних виняткових ситуаціях, що не

належать до бібліотеки STL: bad_alloc, bad_cast, bad_typeid і bad_exception.

Виняткові ситуації, що належать цим класам, поєднує одна загальна властивість

— усі вони об'єктами, а не вказівниками. Отже, вони передаються або за

значенням, або по посиланню.

Клас bad_alloc

Мабуть, найбільш важлива виняткова ситуація описується класом std::bad_alloc.

Виняткова ситуація класу bad_alloc

#include <iostream>

#include <malloc.h>

#include <exception>

using namespace std;

int main()

{

long* q = NULL;

try { q = new long[5000000000000];}

Page 149: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

catch(const bad_alloc&){ cout << "Bad_alloc"; }

catch(...){cout << "Інші";}

cout << q << endl;

delete[] q;

return 0;

}

Ця програма не обов'язково викликає виняткову ситуацію. Однак якщо

виникне недостача пам'яті, буде згенерирований об'єкт класу bad_alloc, а на

екран буде виведена назва виняткової ситуації і нульове значення вказівника q.

Клас bad_cast

Виняткова ситуація bad_cast виникає, коли спроба приведення типу є

некоректною. Наприклад, не можна приводити поліморфний тип до посилання

на інший поліморфний тип.

Виняткова ситуація класу bad_cast

#include <iostream>

#include <typeinfo>

using namespace std;

class A

{

int n;

public:

A(int k):n(k){}

virtual void view(){ cout << n << endl;}

};

class B: public A

{

int m; public:

B(int l): A(l){m=l;}

virtual void view(){ cout << m << endl;}

};

int main()

{

A *p = new A(0);

try

{

B& r = dynamic_cast<B&>(*p); // Генерує виняткову ситуацію

}

catch (const bad_cast& ex)

{

cout << ex.what() << endl;

}

return 0;

}

При виконанні другого оператора в блоці try буде згенерована виняткова

ситуація, повідомлення про яку виводить на екран функція what(), що належить

Page 150: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

класу bad_alloc. Зверніть увагу на те, що об'єкт класу bad_cast передається за

допомогою константного посилання.

Клас bad_typeid

Виняткова ситуація, що належить класу bad_typeid, генерується, якщо

оператор dynamic_cast застосовується до нульового вказівника. Повернемося до

попереднього прикладу, злегка змінивши його.

Виняткова ситуація класу bad_typeid

#include <iostream>

#include <typeinfo>

using namespace std;

class A

{

int n; public:

A(int k):n(k){}

virtual void view(){ cout << n << endl;}

};

class B: public A

{

int m;

public:

B(int l):A(l){ m = l;}

virtual void view(){ cout << m << endl;}

};

int main()

{

A* p = new A(0);

try

{

B* p = dynamic_cast<B*>(p);

// Некоректне

приведення

cout << typeid(*p).name() << endl;

}

catch (const bad_typeid& ex)

{

cout << ex.what() << endl;

}

return 0;

}

У цій програмі виконується спроба привести вказівник на базовий клас до

вказівника на похідний клас. Однак у даному випадку це приведення неможливе,

оскільки вказівник p посилається на об'єкт базового класу A. Для того щоб це

приведення стало коректним, варто було б установити цей вказівник на об'єкт

класу B. Таким чином, реагуючи на помилку, оператор dynamic_cast повертає

Page 151: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

нульовий вказівник. Спроба розіменувати його, виконуючи оператор typeid,

породжує виняткову ситуацію bad_typeid.

Клас bad_exception

Виняткова ситуація bad_exception генерується функцією unexpected() у тих

випадках, для яких не була передбачена обробка. Повернемося приміром, що

ілюструє застосування списку дозволених виняткових ситуацій. Припустимо, у

нас немає бажання перелічувати усі виняткові ситуації, що можуть виникнути

при виконанні програми, оскільки програма повинна однаково реагувати на них

(наприклад, виводити повідомлення на екран).

Виняткова ситуація класу bad_exception

#include <iostream>

#define MAX

1000000 using

namespace std;

int Check(int) throw (const char*, bad_exception); int

Divide(int, int);

int main()

{

int n = MAX, m=0, l;

printf("Початок\n");

try

{

printf("Усередині блоку try\n"); l

= Divide(n+1,m);

}

catch (const char* s)

{

printf("%s\n",s);

}

catch (int k)

{

printf("Число чи більше дорівнює %d\n",k);

}

printf("Кінець\n");

return 0;

}

int Check(int i) throw (const char*, bad_exception)

{

printf("Усередині функції Check\n");

if(i==0) throw("Розподіл на нуль усередині функції main!");

if(abs(i)>=MAX) throw "Bad_exception...";

printf("Кінець функції Check\n");

return i;

}

Page 152: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

int Divide(int n, int m)

{

printf("Усередині функції Divide\n");

n=Check(n);

m=Check(m);

printf("Кінець функції Divide\n");

return n/m;

}

Нагадаємо, що ця програма виконує розподіл двох цілих чисел. Тепер для

порівняння знаменника з нулем і максимально припустимим значенням

використовується одна функція — Check(). Вона реагує на ситуацію, коли

знаменник дорівнює нулю. В усіх інших випадках функція unexpected() виявляє

непередбачену виняткову ситуацію і генерує об'єкт класу bad_exception.

Початок

Усередині блоку try

Усередині функції Divide

Усередині функції Check

Bad_exception...

Кінець

У завершення відзначимо, що обробка виняткових ситуацій приводить до

додаткових витрат ресурсів комп'ютера. Програми, що передбачають обробку

виняткових ситуацій, містять додатковий код і працюють повільніше. Таким

чином, до цього механізму мови С++ варто прибігати тільки в дійсно виняткових

випадках, коли без нього не можна обійтися. Наприклад, у попередній програмі

ігнорування виняткових ситуацій зменшує час її виконання в два рази.

Резюме

1. Винятковою ситуацією називається будь-яка подія, що вимагає особливої

обробки.

2. Перевірка умов, що описують виняткову ситуацію, і реакція на її

виникнення називається обробкою виняткової ситуації. Ця задача

покладається на оброблювача виняткової ситуації.

3. Обробка виняткових ситуацій у мові С++ є об’єктно-орієнтованою. Це

значить, що виняткова ситуація є об'єктом, що генерується при виникненні

незвичайних умов, передбачених програмістом, і передається

оброблювачу, що неї перехоплює. Об'єктом, що описує природу

виняткової ситуації, може бути будь-як сутність — літерал, рядок, об'єкт

класу, число і т.д. Не слід думати, що виняткова ситуація обов'язково

повинна бути об'єктом якого-небудь класу.

4. В основі обробки виняткових ситуацій три ключових слова: try, catch і

throw. Оператор try визначає область програми, де може виникнути

виняткова ситуація, оператор throw генерує об’єкт — виняткову ситуацію,

а оператор catch перехоплює цей об’єкт.

Page 153: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

5. Розмір блоку try не обмежений. У нього можна занурити як один оператор,

так і цілу програму.

6. Кожен блок catch відповідає окремому типу виняткової ситуації. Програма

сама визначає, який з них виконати. У цьому випадку інші блоки catch не

виконуються. Кожен блок catch має аргумент, що приймає визначене

значення. Цей аргумент може бути об'єктом будь-якого типу. Якщо

програма виконана правильно й у блоці try не виникло жодної виняткової

ситуації, усі блоки catch будуть зігноровані.

7. Якщо в програмі виникла подія, що програміст вважає небажаним,

оператор throw генерує виняткову ситуацію. Для цього оператор throw

повинний знаходитися усередині блоку try або усередині функції,

викликуваної усередині блоку try.

8. Якщо в програмі виникла виняткова ситуація, для якої не передбачені

перехоплення й обробка, викликається стандартна функція terminate(), що,

у свою чергу, викликає функцію abort()

9. Якщо блок try знаходиться усередині функції, обробка виняткової ситуації

виконується при кожнім виклику.

10. Блоки try і catch нерозривні. Не можна помістити блок try у функцію,

залишивши блок catch у функції main(). Необхідно або обробити виняткову

ситуацію усередині функції, як це зроблено в більш ранньому прикладі,

або перенести обробку в модуль виклику. У першому випадку функція,

завершивши обробку, повертає визначене її специфікацією значення, а в

другому — виняткову ситуацію.

11. Оскільки виняткова ситуація може бути об'єктом класу, у мові С++ існує

можливість створювати ієрархію виняткових ситуацій. У цьому випадку

блок catch перехоплює об'єкти не тільки базового, але і похідних класів.

При генерації похідних виняткових ситуацій це приводить до

непорозумінь — їх перехоплює блок catch, призначений для обробки

базових виняткових ситуацій.

12. Для того щоб реагувати на виняткові ситуації, обробка яких програмістом

не передбачена, у мові С++ використовуються функції terminate() і

unexpected(), оголошені в заголовку <exception>, а також функція abort(),

прототип якої міститься в заголовках <process.h> і <stdlib.h>.

13. Ідентифікатори new_unexpected і new_terminate — імена нових

оброблювачів. Аргументами функцій set_unexpected() і set_terminate() є

вказівники на ці оброблювачі. Оскільки цими вказівниками автоматично є

імена нових функцій, синтаксична конструкція виглядає досить просто.

Одержуючи адресу нового оброблювача, функції set_unexpected() і

set_terminate() повертають адреси старих оброблювачів. Це дозволяє

зберегти їх у пам'яті і відновити при необхідності стару систему обробки

непередбачених ситуацій. Заступники функцій unexpected() і terminate() не

повинні повертати керування програми. Вони зобов'язані поводитися, як

їхні прототипи.

Page 154: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

Лабораторна робота №1

Введення в програмування за допомогою API

Мета роботи:

1. Ознайомитися з особливостями Win32 API , типами програм, особливостями

виклику функцій API.

2. Перевірити принципи роботи функції MessageBoxA, GetComputerNameA,

GetCurrentDirectoryA, GetDriveTypeA, GetSystemDirectoryA, GetTempPathA,

GetUserNameA, GetWindowsDirectoryA

Основні теоретичні відомості

Win32 API (розшифровується як інтерфейс прикладних програм) - це множина

підпрограм-функцій, на яких побудована операційна система WINDOWS, яка

використовує 32х-бітну адресацію, починаючи з WINDOWS 95 і закінчуючи

WINDOWS XP. Розробники WINDOWS зробили багато зусиль щоб

стандартизувати як назви функцій, так і їх параметри. Тому використовувати їх

не так важко, якщо засвоїти деякі загальні концепції.

Більшість функцій доступні для програм користувача, які написані для Windows

на будь-якій мові програмування (у тому числі і на асемблері). Множина цих

функцій розширюється при переході до наступної версії Windows, таким чином,

забезпечується сумісність розроблених раніше програм із новими версіями

операційної системи. Існують і функції, які не відображені в документації, або

для свого застосування вимагають від програми спеціальних прав доступу до

пам'яті.

Суть функцій API зрозуміти значно легше, якщо уявити, з яких файлів вони

викликаються і на які групи ці функції поділяються. Асемблер - це як раз той

зручний і простий засіб, який дозволить вам звертатись безпосередньо до будь-

якої функції API, що знаходиться у DLL-файлі.

Секрет пізнання операційної системи через програмування на асемблері полягає

у тому, що сам асемблер не накладає жодних обмежень на програму та дані, з

якими вона працює. Це повинен робити сам програміст з метою захисту

операційної системи від своїх некоректних дій. Таким чином, основною метою

системного програмування є написання коректних програм з необмеженими

можливостями (в рамках операційної системи). Для збереження коректності ми

будемо користуватися певними правилами програмування, які будуть зрозумілі

на конкретних прикладах.

Типи програм. Процесори стандарту Intel можуть працювати в трьох основних

режимах: реальному, віртуальному і захищеному. При включенні комп'ютера

його процесор працює в реальному режимі. Після завантаження операційної

системи (ОС) процесор може бути переключений програмами ОС в інші

режими. В реальному та віртуальному режимах використовується 16-бітна

адресація з фіксованими сегментами по 64К. У захищеному режимі

використовується 32х-бітна адресація з необмеженими сегментами, і адреса до

Page 155: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

пам'яті формується (на апаратному рівні) за допомогою дескрипторних таблиць,

в яких задаються початкові адреси сегментів, їх довжина, та права доступу до

пам'яті і до портів для процесів, які їх використовують. Крім того, в захищеному

режимі реалізоване апаратне переключення між задачами за допомогою

спеціальних таблиць.

Особливості виклику функцій API. Найбільш перспективним з точки зору

програмування є захищений режим, тому що він використовує всі апаратні

можливості комп'ютера. Отже, функції API для Windows відіграють ту ж саму

роль, що і переривання INT 21h для DOS в реальному або віртуальному режимі,

але, відмінності між ними досить суттєві. Перелічимо їх:

Функції API не відміняють, а заміняють програмні переривання.

Механізм обробки апаратних переривань залишається на рівні драйверів

пристроїв;

Стандарт виклику функцій API оснований на передачі параметрів

через стек (а не через регістри);

Значення кожної функції повертається в регістрі EAX. Якщо функція

повертає структуру даних, то регістр EAX містить логічну ознаку

виконання, а адресу структури необхідно передати до функції як параметр;

Функції API працюють у захищеному режимі процесора, а

переривання DOS - у реальному чи віртуальному режимі.

Функції API зберігаються у різних бібліотеках динамічного компонування, які

знаходяться у файлах із розширенням DLL, наприклад, kernel32.dll, user32.dll,

gdi32.dll та ін. Ці файли знаходяться у системному каталозі Windows

(наприклад, "C:\Windows\System"). У разі необхідності, програміст може

створити DLL-файл з набором своїх функцій.

Програми Windows звертаються до функцій API за допомогою команд

апаратного виклику CALL, наприклад: call MessageBoxA, де MessageBoxA -

32х-бітна адреса функції. Саме ця назва функції фігурує у файлі user32.dll

(подивіться редактором цей файл). Перелік можливих функцій є у файлі

H:/tasm/lib/Import32.lib, який називається бібліотекою імпорту.

Параметри для виконання будь-якої функції API перед її викликом повинні

засилатися в стек, починаючи з останнього параметра (див. текст програми).

Тому кожний параметр є 32х-бітним числом (в якому можуть

використовуватись не всі біти). Параметрами досить часто бувають спеціальні

дескриптори - хендли (HANDLE) та атоми.

Дескриптори (хендли та атоми) - це унікальні цілі числа, які Windows

використовує для ідентифікації об'єктів, які створюються або використовуються

в системі. Хендли займають по 4 байти, а атоми - по 2 байти. Хендли

ідентифікують вікна, меню, блоки пам'яті, екземпляри програми, пристрої

виводу, файли, аудіо та відео потоки, та інші об'єкти. Атоми ідентифікують

Page 156: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

стандартні іконки, курсори та об'єкти, які не змінюються при наступному

завантаженні системи.

Більшість дескрипторів є значеннями індексів внутрішніх таблиць, які Windows

використовує для доступу та керування своїми об'єктами. Звичайно, програми

користувача (ужитки) в захищеному режимі не мають прав доступу до цих

таблиць. Тому, коли необхідно отримати чи змінити дані, що пов'язані з певним

об'єктом Windows, ужиток використовує відповідну функцію API з параметром

хендла цього об'єкту. Таким чином Windows забезпечує захист своїх даних при

роботі у багатозадачному режимі.

ПОРЯДОК ВИКОНАННЯ РОБОТИ

1. Створити файл tasm32.bat для компіляції програм для 32х-бітного режиму:

del %1.exe

h:\tasm\bin\tasm32 %1.asm /mx/m

h:\tasm\bin\tlink32 %1.obj,,, h:\tasm\lib\import32.lib /Tpe /aa

del %1.obj

del %1.map

2. Набрати подану нижче програму для визначення імені комп'ютера (яке

задається системним адміністратором при установці операційної системи),

зберегти її у файлі з розширенням ".ASM ".

3. Відкомпілювати набрану програму за допомогою командного рядка:

tasm32.bat <назва файлу програми без розширення>

4. Запустити одержаний exe-файл на виконання. Записати ім'я комп'ютера, яке

отримала програма.

5. Розглянути текст програми, вивчити загальну структуру програми із

застосуванням функцій Win32 API. Переписати в зошит текст програми з

відповідними коментарями.

6. Замінити виклик функції API GetComputerNameA на виклик однієї з

наступних функцій, змінивши відповідним чином параметри:

GetUserNameA, GetWindowsDirectoryA, GetSystemDirectoryA, GetTempPathA,

GetCurrentDirectoryA, GetDriveTypeA, після чого відкомпілювати і запустити

програму.

Текст програми

.386 ; Для процесора не нижче INTEL-386

.model flat, STDCALL ; компілювати як програму для WIN32;

Page 157: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

; Визначення зовнішніх процедур:

extrn ExitProcess :proc ; Ліквідація процесу,

extrn MessageBoxA :proc ; Вивід вікна з повідомленням,

extrn GetComputerNameA :proc ; Отримання імені комп'ютера.

.data ; Вміст сегменту даних:

buflen dd 256 ; Визначення комірки пам'яті

hello_title db ' Лабораторна робота № 1 ', 0

hello_message db 'Computer Name: ' ; Рядок байтів

user_name db 256 dup (0) ; Буфер заповнений нулями

.code ; Вміст сегменту коду:

Start:

push offset buflen ; 2-й параметр: адреса buflen

push offset user_name ; 1-й параметр: адреса user_name

call GetComputerNameA ; виклик функції API

push 40h ; стиль вікна - одна кнопка "OK" з піктограмою "і"

push offset hello_title ; адреса рядка із заголовком

push offset hello_message ; адреса рядка з повідомленням

push 0 ; хендл програми-власника вікна

call MessageBoxA ; виклик функції API

push 0 ; код виходу з програми

call ExitProcess ; завершення програми

end Start ; закінчення сегменту кода

Можливі помилки:

рядок з коментарем не позначений символом ";"

назва функції API не визначена в команді extrn;

записана маленька буква замість великої або навпаки;

хибна послідовність параметрів функції при записі в стек;

перед викликом функції в стек записані не всі параметри;

невірний тип параметрів (word або dword);

передача змінної як параметра замість її адреси (offset).

Про параметри функцій можна дізнатися з довідкового файлу WIN32.HLP, який

знаходиться в каталозі C:\Program Files\Common Files\Borland

Shared\MSHelp\win32.hlp. Для отримання довідки необхідно запустити файл

WIN32.HLP, вибрати розділ "index (указатель)", набрати назву функції без

останньої букви "A". Всі параметри, які будуть вказані в довіднику, необхідно

засилати в стек, починаючи з останнього. Якщо назва параметру починається з

наступних букв, то його довжина вказана у таблиці:

Page 158: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

Перші

символи

Довжина

параметра Зміст параметра

H 4 Хендл об'єкту

Lp 4 Адреса об'єкту (offset)

N 4 Змінна або адреса змінної (4 байти),

що містить кількість

U або B 4 Ідентифікатори типу BOOL або

прапорці

W 2 Ціле 16-ти бітне число

D 4 Ціле 32х бітне число

Short 2 Ціле 16ти бітне число

Long 4 Ціле 32х бітне число

Довідкова інформація по функціях API, що використані в роботі

GetComputerNameA - отримує ім'я комп'ютера з реєстру Windows

LPTSTR lpBuffer // адреса буфера для імені;

LPDWORD nSize // адреса довжини буфера.

GetCurrentDirectoryA - отримує поточний каталог

DWORD nBufferLength // довжина буфера в байтах;

LPTSTR lpBuffer // адреса буфера для імені поточного каталогу.

GetDriveTypeA - отримує інформацію про тип дискового пристрою

LpRootPathName // адреса рядка з назвою кореневого каталогу;

Результат в регістрі EAX:

0 - Диск не існує;

1 - Каталог не існує;

2 - Пристрій для змінних дисків;

3 - Пристрій для жорстких дисків;

4 - Мережний диск;

5 - Пристрій для читання компакт-дисків;

6 - Віртуальний диск в оперативній пам'яті.

GetSystemDirectoryA - отримує повне ім'я системного каталогу

LPTSTR lpBuffer // адреса буфера для імені;

UINT uSize // довжина буфера.

GetTempPathA- отримує шлях до каталогу з тимчасовими файлами

DWORD nBufferLength // довжина буфера в байтах;

LPTSTR lpBuffer // адреса буфера для каталогу;

Page 159: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

Результат в регістрі EAX : кількість символів, записаних у буфер.

GetUserNameA - отримує ім'я (login) поточного користувача

LPTSTR lpBuffer // адреса буфера для імені;

LPDWORD nSize // адреса довжини буфера.

GetWindowsDirectoryA - отримує повний шлях до каталогу Windows

LPTSTR lpBuffer // адреса буфера для імені;

UINT uSize // довжина буфера.

Зміст звіту

1. Мета роботи.

2. Короткі теоретичні відомості і розрахункові формули.

3. Вихідні дані для виконання роботи (відповідно до отриманого

варіанта).

4. Результати виконання роботи, отримані при різних вихідних даних.

5. Контрольні запитання

6. Висновки.

КОНТРОЛЬНІ ЗАПИТАННЯ

1. Що таке захищений режим процесора ?

2. Що таке функція API ?

3. Що таке дескриптори, хендли, атоми ?

4. Що таке бібліотека динамічного компонування (DLL) ?

5. Як визначити, які функції є у певному DLL-файлі ?

6. Яким чином передаються параметри функціям API ?

7. У якому регістрі знаходиться результат виконання функції ?

8. Яка можлива довжина кожного параметра ?

9. Яка загальна структура програм при програмуванні під Windows ?

10. Що означають ключові слова data і code ?

11. Що роблять функції MessageBoxA, GetComputerNameA,

GetCurrentDirectoryA, GetDriveTypeA, GetSystemDirectoryA,

GetTempPathA, GetUserNameA, GetWindowsDirectoryA ?

Page 160: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

Лабораторна робота №2

Структури даних для використання системних функцій

Мета роботи:

1. Ознайомитися з особливостями cтруктур даних для використання системних

функцій

2. Перевірити принципи роботи функції GetLogicalDriveStringsA і чим вона

відрізняється від функції GetLogicalDrives

Основні теоретичні відомості

Для використання більш складних функцій API необхідно використовувати

спеціальні структури даних. Вся концепція програмування в WINDOWS

основана на чіткому впорядкуванні структур даних, пов'язаних із ними програм-

функцій та констант-параметрів. Дані, що пов'язані з виконанням одної функції

об'єднуються в блок певної довжини (він називається структурою). Елементами

таких структур можуть бути інші структури і т.д. Існують структури загального

користування, які не пов'язані з конкретною функцією API, а містять довідкову

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

цих структурах постійно змінюються і деколи їх неможливо передбачити. Тому

для отримання цих даних не можна користуватися звичайними командами

читання з пам'яті типу "mov". Перед користуванням такою динамічною

інформацією необхідно перевірити, чи готова інформація для споживання. Щоб

уникнути некоректності при умові паралельного виконання процесів кожне

звернення до системної інформації необхідно регіструвати. Тому фактично

існує два типа функцій API:

1. функції, які виконують будь-яку дію;

2. функції, які отримують будь-яку інформацію.

У даній лабораторній роботі треба отримати та розшифрувати одну з таких

довідкових структур, яка містить інформацію про системний час. Для виклику

довільної довідкової функції необхідно вказати адресу місця в пам'яті, куди

буде записана довідкова структура. Цю адресу, як параметр функції, завжди

перед викликом функції треба опустити в стек.

В лабораторній роботі необхідно також звернути увагу на те, що в програмі

можна використовувати не лише функції API, але і функції різних мов

програмування. Для цього необхідно, щоб в системі були присутні відповідні

DLL-файли. Наприклад, можна викликати функцію виводу wsprintf, яка

використовується в мові C. Ця функція передає управління до функції

_wsprintfA, що міститься у файлі USER32.DLL. Функції такого типу можуть не

звільняти після свого виконання стек. В такому випадку програміст сам повинен

про це подбати. Отже, оскільки кожний параметр у стеку займає 4 байти, то

після виклику такої функції необхідно записати add esp,4*N, де N - кількість

параметрів для виконання функції (див. текст програми).

Page 161: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

ПОРЯДОК ВИКОНАННЯ РОБОТИ

1. Набрати подану нижче програму для визначення поточної дати та місцевого

часу, зберегти її у файлі з розширенням ".ASM".

2. Відкомпілювати набрану програму та запустити одержаний exe-файл на

виконання.

3. Розглянути текст програми, знайти виклик підпрограми _wsprintfA для

перетворення двійкового значення в рядок десяткових цифр.

4. Додати до виводу значення секунд та мілісекунд.

5. Змінити текст програми таким чином, щоб функція API MessageBoxA

виводила на екран вікно з двома кнопками: <OK> та <Cancel>. При натисненні

кнопки <OK> програма повинна знову отримати системний час, а при

натисненні кнопки <Cancel> - програма завершує роботу. Для цього треба

знайти у файлі H:/tasm/include/win119.inc константу MB_OKCANCEL та вказати

її як стиль вікна MessageBoxA. Після відповіді користувача треба порівняти

регістр EAX з константою IDOK за допомогою асемблерної команди "cmp eax,

IDOK". Якщо значення однакові, то зробити перехід на початок програми по

команді "jz Start".

Текст програми

; Програма для визначення поточної дати та місцевого часу

.386

.model flat,STDCALL

extrn ExitProcess: proc

extrn GetLocalTime: proc

extrn MessageBoxA: proc

extrn _wsprintfA:Proc

.data

Time_title db ' Лабораторна робота №2',0

TIME_STRING db 2000 dup (0)

FORMAT_STRING:

db ' Системний час:',0dh,0ah,0dh,0ah

db ' Рiк: %ld',0dh,0ah

db ' Мiсяць: %ld',0dh,0ah

db ' День тижня: %ld',0dh,0ah

db ' Число: %ld',0dh,0ah

db ' Година: %ld',0dh,0ah

db ' Хвилин: %ld',0dh,0ah

db 0

Page 162: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

Time_struc:

wYear dw 0 ; Рік

wMonth dw 0 ; Місяць

wDayOfWeek dw 0 ; День тиждня

wDay dw 0 ; Число

wHour dw 0 ; Година

wMinute dw 0 ; Хвилина

wSecond dw 0 ; Секунда

wMilliseconds dw 0 ; Мілісекунда

;============================================================

==

.code

Start: push offset Time_struc

call GetLocalTime

xor eax,eax ; EAX=0

mov ax,wMinute

push eax ; наступні параметри з стеку не забираються

mov ax,wHour

push eax

mov ax,wDay

push eax

mov ax,wDayOfWeek

push eax

mov ax,wMonth

push eax

mov ax,wYear

push eax

push offset FORMAT_STRING

push offset TIME_STRING

call _wsprintfA ; Вивід параметрів

add esp,4*8 ; та очищення стеку

push 0h

push offset Time_title

push offset TIME_STRING

push 0

call MessageBoxA

push 0

call ExitProcess

end Start

6. Замінити виклик функції API GetLocalTime на виклик однієї з наступних

функцій (змінивши відповідним чином параметри): GetSystemTime, DeleteFileA,

Page 163: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

CopyFileA, RemoveDirectoryA, SetCurrentDirectoryA, SetVolumeLabelA,

GetTempFileNameA, SetFileAttributesA, GetFileAttributesA, GetLogicalDrives,

GetLogicalDriveStringsA. Для кожної функції додати відповідні дані та команди,

якщо вони потрібні.

7. Відкомпілювати та налагодити програму для правильної роботи з кожною

функцією, що вказана у попередньому пункті. В звіті відобразити всі зроблені

програми.

Довідкова інформація по функціях API, що використані в роботі

CopyFileA - копіює файл

LpExistingFileName // вказівник на ім'я файлу, що копіюється;

LpNewFileName // вказівник на ім'я файлу-копії;

BOOL bFailIfExists // якщо значення - ненульове, то в разі існування файлу-

копії, функція не виконується; якщо значення - нульове - файл перезаписується.

DeleteFileA - знищує заданий файл

LPCTSTR lpFileName // вказівник на ім'я файлу, який знищується.

GetFileAttributesA - отримує атрибути заданого файлу або каталогу

LPCTSTR lpFileName // адреса імені файлу або каталогу;

В результаті регістр EAX буде містити побітну інформацію про атрибути файла.

Нижче наведений перелік номерів бітів і атрибутів, що їм відповідають:

0 - READONLY файл (каталог) тільки для читання;

1 - HIDDEN файл (каталог) є прихованим;

2 - SYSTEM файл (каталог) виключно використовується операційною

системою;

4 - DIRECTORY "файл або каталог" є каталогом;

5 - ARCHIVE файл (каталог) є архівним. Автоматично встановлюється, якщо

файл (каталог) переносився;

7 - NORMAL файл (каталог) немає інших атрибутів;

8 - TEMPORARY файл для тимчасового зберігання даних, знищується після

виконання програми, яка його створює;

11 - COMPRESSED файл (каталог) є стиснутим;

12 - OFFLINE дані у файлі тимчасово є недоступними.

GetLocalTime - заповнює структуру lpSystemTime значенням місцевого часу та

дати

LpSystemTime // адреса структури системного часу, яка буде заповнена (див.

текст програми);

GetLogicalDrives - отримує інформацію про логічні диски, наявні в системі (0-й

біт в регістрі EAX - диск A, 1-й - диск B і т.д.)

Page 164: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

GetLogicalDriveStringsA - отримує інформацію про шляхи до кореневих

каталогів логічних дисків, наявних в системі

DWORD nBufferLength // довжина буферу;

LPTSTR lpBuffer // адреса буферу для рядка імен логічних дисків, розділених

символами нуля. Ім'я останнього диску закінчується двома нулями.

GetSystemTime - заповнює структуру lpSystemTime значенням поточного часу за

Грінвичем (див. також функцію GetLocalTime).

GetTempFileNameA - отримує ім'я тимчасового файлу для використання

програмою-ужитком

LPCTSTR lpPathName // адреса імені каталогу для тимчасового файлу;

LPCTSTR lpPrefixString // адреса префіксу імені тимчасового файлу;

UINT uUnique // число, яке з'єднується з префіксом для створення імені

тимчасового файлу (0-випадкове число);

LPTSTR lpTempFileName // адреса буферу для імені тимчасового файлу.

RemoveDirectoryA - знищує заданий каталог

LPCTSTR lpPathName // адреса каталогу, який знищується.

SetCurrentDirectoryA - встановлює поточний каталог

LPCTSTR lpPathName // адреса імені нового поточного каталогу.

SetFileAttributesA - встановлює атрибути файлу (див. функцію

GetFileAttributesA)

LPCTSTR lpFileName // адреса імені файлу;

DWORD dwFileAttributes // 32х-бітна константа, що задає атрибути файлу.

SetVolumeLabelA - встановлює мітку тому для диску

LPCTSTR lpRootPathName // адреса імені кореневого каталогу;

LPCTSTR lpVolumeName // адреса імені мітки.

Зміст звіту

1. Мета роботи.

2. Короткі теоретичні відомості і розрахункові формули.

3. Вихідні дані для виконання роботи (відповідно до отриманого

варіанта).

4. Графічні результати виконання роботи, отримані при різних вихідних даних.

5. Контрольні запитання

6. Висновки

КОНТРОЛЬНІ ЗАПИТАННЯ

1. На які два основні типи поділяються функції API?

2. Що таке структура даних у Win32 API?

3. Для чого існують структури даних?

4. Як передаються структури даних у системні функції?

Page 165: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

5. Звідки беруться шаблони структур даних?

6. Що таке константа-параметр?

7. Де можна знайти перелік стандартних констант WINDOWS?

8. Чим і наскільки відрізняється місцевий час від системного?

9. Що таке файл?

10. Для чого використовуються тимчасові файли?

11. Що таке атрибут файлу?

12. Які ви знаєте функції для роботи з файлами та пристроями?

13. Що таке мітка диску?

14. Що таке поточний каталог?

15. Для чого потрібні тимчасові імена файлів і як їх отримати?

16. Що робить функція GetLogicalDriveStringsA і чим вона відрізняється

від функції GetLogicalDrives?

Лабораторна робота № 3

Робота з пам'яттю

Мета роботи:

1. Ознайомитися з особливостями пам'яті, як основним ресурсом при

програмуванні в багатозадачному середовищі.

2. Перевірити принципи роботи програми, яка виділяє блок пам'яті зчитує у

виділений фрагмент файл виводить його на екран та звільняє виділений блок.

Основні теоретичні відомості

Пам'ять є основним ресурсом при програмуванні в багатозадачному середовищі.

Множина вільних фрагментів пам'яті називається хіп (від англійського слова

HEAP). Програміст може виділити для своєї програми блок пам'яті будь-якої

довжини, що не перевищує загальний об'єм вільної пам'яті. В Windows пам'ять

виділяється в 2 етапи:

спочатку система виділяє фрагмент віртуальної пам'яті, який отримує

свій хендл, але не отримує реальної адреси;

потім система розміщує (блокує) цей фрагмент у реальній пам'яті і

фрагмент отримує початкову адресу.

Після того, як програміст отримує адресу початку виділеного блоку, він може її

використовувати. Комірки з адресами до початку та після кінця блоку

використовувати не можна, тому що вони належать іншим програмам або

системі.

Після того, як програма використала блок пам'яті, його необхідно розблокувати.

Таким чином, він знову стає віртуальним, і при необхідності може бути

переміщений системою в інше місце або на диск. Якщо програма довго не

розблоковує блок пам'яті, то це негативно відображається на продуктивності

операційної системи в цілому. Отже, якщо після розблокування пам'яті її знову

заблокувати, адреса початку блоку може бути іншою. Якщо програміст взагалі

відмовляється від використання виділеного блоку пам'яті, він повинен звільнити

Page 166: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

блок, що веде до знищення його хендла. Очевидно, що перед звільненням блоку

пам'яті його необхідно розблокувати.

В лабораторній роботі запропонована програма, яка виділяє блок пам'яті зчитує

у виділений фрагмент файл виводить його на екран та звільняє виділений блок.

Для відкриття, читання та запису файлів використовуються відповідні функції

API, опис яких ви знайдете після тексту програми.

ПОРЯДОК ВИКОНАННЯ РОБОТИ

1. Набрати подану нижче програму, зберегти її у файлі з розширенням ".ASM".

2. Відкомпілювати набрану програму та запустити одержаний exe-файл на

виконання.

3. Розглянути текст програми, знайти функції виділення, блокування,

розблокування та звільнення пам'яті.

4. Доповнити подану програму функцією вибору файлу GetOpenFileNameA

(див. довідкову інформацію після програми).

5. Доповнити отриману програму фрагментом для визначення часу створення та

часу останньої модифікації файлу за допомогою функції GetFileTime.

6. Перетворити час у зручний для сприйняття вигляд за допомогою функції

FileTimeToSystemTime та вивести його за допомогою програми попередньої

лабораторної роботи. Структуру даних часу time_struc розмістити у виділеному

блоці пам'яті.

7. Після закриття файлу виконати його запуск через оболонку WINDOWS за

допомогою команди ShellExecuteA.

8. Кожний варіант програми оформити в звіті і відповісти на контрольні

запитання.

Текст програми

.386

.model flat,STDCALL

extrn CreateFileA:Proc

extrn GlobalAlloc:Proc

extrn GlobalLock:Proc

extrn ReadFile:Proc

extrn GetLastError:Proc

extrn MessageBoxA:Proc

extrn CloseHandle:Proc

Page 167: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

extrn GlobalUnlock:Proc

extrn GlobalFree:Proc

extrn ExitProcess:Proc

.data

title1 db 'Лабораторна робота №3',0

openname db 'ReadFile.asm',250 dup(0)

hFile dd 0

hMemory dd 0 ; Хендл блоку пам'яті

pMemory dd 0 ; Адреса блоку пам'яті

SizeRead dd 0

MEMSIZE equ 1000000h ; 16 Mb

.code

Start: push 0

push 0h ; файл з довільними атрибутами

push 4h ; відкрити існуючий або створити новий

push 0 ; без атрибутів безпеки

push 1h ; дозволено спільний доступ по читанню

push 80000000h ; читати файл

push offset openname ; адреса імені файлу

call CreateFileA

mov hFile,eax ; отримали хендл файлу

push MEMSIZE

push 2h + 40h ; GMEM_MOVEABLE + GMEM_ZEROINIT

call GlobalAlloc

mov hMemory,eax ; отримали хендл блоку пам'яті

push hMemory

call GlobalLock ; розмістили блок

mov pMemory,eax ; та отримали його адресу

push 0

push offset SizeRead ; адреса змінної скільки байтів прочитано

push MEMSIZE-1 ; скільки байтів можна прочитати

push pMemory ; адреса початку блоку

push hFile ; хендл файлу

call ReadFile ; функція читання файлу

push 0

push offset title1

push pMemory ; вивід блоку пам'яті на екран

push 0

call MessageBoxA

Page 168: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

push hFile

call CloseHandle ; закрити файл

push hMemory

call GlobalUnlock ; розблокувати блок пам'яті

push hMemory

call GlobalFree ; звільнити блок пам'яті

push 0

call ExitProcess ; повернення у Windows

end Start

Довідкова інформація по функціях API, що використані в роботі

CreateFileA - створення або визначення хендлу існуючого файлу

lpFileName // вказівник на ім'я файлу;

dwDesiredAccess // режим доступу:

80000000h - читання, 40000000h - запис;

dwShareMode // режим спільного доступу:

1 - по читанню, 2 - по запису;

lpSecurityAttributes // вказівник на структуру атрибутів безпеки;

dwCreationDistribution // метод створення:

1 - створювати лише тоді, коли не існує; 2 - створювати завжди новий; 3 -

відкрити лише існуючий; 4 - відкрити існуючий або створити новий; 5 - обрізати

існуючий до нуля;

dwFlagsAndAttributes // атрибути файлу;

HANDLE hTemplateFile // режим передачі файлу процесу.

CloseHandle - закриття відкритого хендлу

hObject // хендл об'єкту, який треба закрити.

ReadFile - читання файлу

hFile // хендл файлу;

lpBuffer // адреса блоку пам'яті в який попадуть дані;

nNumberOfBytesToRead // кількість байтів, що треба зчитати;

lpNumberOfBytesRead // адреса кількості зчитаних байтів;

lpOverlapped // адреса структури спільного використання або 0.

GlobalAlloc - резервування віртуальної пам'яті та отримання хендлу

віртуального блоку пам'яті

UFlags // тип блоку пам'яті та його атрибути;

dwBytes // довжина блоку в байтах;

Результат в регістрі EAX: хендл віртуального блоку пам'яті.

Page 169: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

GlobalLock - розміщення віртуального блоку за певною адресою

hMem // хендл віртуального блоку пам'яті;

Результат в регістрі EAX: адреса блоку в пам'яті.

GlobalUnlock - розблокування блоку пам'яті, дозвіл на переміщення

HMem // хендл блоку пам'яті.

GlobalFree - звільнення блоку пам'яті

HMem // хендл блоку пам'яті.

GetOpenFileNameA - функція діалогу з користувачем, в результаті якого

користувач вибирає файл для відкриття

LPOPENFILENAME // адреса структури яка визначає діалог:

FSize dd 76 ; довжина цiєї структури;

Howner dd 0 ; вказiвник вiкна-власника або 0;

AppHWnd dd 0 ; вказiвник модуля-власника;

Filters dd offset filter_tab ; вказiвник на перелiк типiв файлiв;

CustFilters dd 0 ; вказiвник на перелiк типiв файлiв якi дозволенi користувачевi;

CstFltSize dd 0 ; довжина буферу на який вказує CustFilters;

CurFilter dd 3 ; iндекс вибраного фiльтру (1,2,3,...) або 0;

CurFileName dd offset openname ; вказiвник на повне iм'я файлу, наприклад, на

"С:\dir1\dir2\file.ext",0;

CurFlNmSize dd 512 ; довжина буферу вказаного в CurFileName;

CurFile dd 0 ; вказiвник на iм'я файлу з розширенням;

CurFlSize dd 0 ; довжина буферу вказаного в CurFile;

InitialDir dd offset dir ; вказiвник на каталог файлу або 0 для даного каталога;

DlgTitle dd offset titl ; вказiвник на назву вiкна;

Flags dd 00h ; тип вiкна, яке вiдкриває файл (може бути 200h);

FileOffset dw 0 ; повертає довжину повного шляху, наприклад=13 якщо

користувач ввів рядок "С:\dir1\dir2\file.asm";

ExtOffset dw 0 ; змiщення вiд початку рядка до розширення (в даному прикладi

=18) або 0 якщо розширення немає;

Extension dd 0 ; вказiвник на стандартне розширення, яке буде додано до iменi,

якщо розширення немає (або 0);

CustData dd 0 ; вказiвник на данi для hook-процедури;

HookProc dd 0 ; вказiвник на hook-процедуру, якщо вона дозволена у Flags;

TmplateRsc dd 0 ; вказiвник на шаблон ресурсiв, якщо вiн передбачений у Flags;

; додаткові змінні до структури:

filter_tab db "Графiчнi файли (*.BMP)",0h,"*.BMP",0

db "Текстовi файли (*.TXT)",0,"*.txt",0

db "Лабораторнi роботи (*.ASM)",0,"*.asm",0

db "Всi типи файлiв (*.*)",0h,"*.*",0,0

dir db "c:\users",0

titl db "Лабораторна робота № 3",0

Page 170: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

GetFileTime - отримує дату і час коли файл був створений, коли останній раз був

прочитаний та коли останній раз змінювався

Hfile // хендл файлу;

lpCreationTime // адреса структури creation_time (8 байт);

lpLastAccessTime // адреса структури access_time (8 байт);

lpLastWriteTime // адреса структури write_time (8 байт).

FileTimeToSystemTime - перетворює 8 байт файлового часу в системний час

lpFileTime, // адреса creation_time, access_time або write_time;

lpSystemTime // адреса структури Time_struc (див. лаб. №2)

ShellExecuteA - відкриває або виконує вказаний файл

Hwnd // хендл до вікна-власника (або 0);

lpOperation // вказівник на рядок, що містить дію (або 0);

lpFile // вказівник на папку або файл, який треба виконати;

lpParameters // вказівник на параметри виконання (або 0);

lpDirectory // вказівник на робочий каталог (або 0);

nShowCmd // прапорець показу відкриття (або 0).

Зміст звіту

1. Мета роботи.

2. Короткі теоретичні відомості і розрахункові формули.

3. Вихідні дані для виконання роботи (відповідно до отриманого

варіанта).

4. Результати виконання роботи, отримані при різних вихідних даних.

5. Контрольні запитання

6. Висновки.

КОНТРОЛЬНІ ЗАПИТАННЯ

1. Що таке реальна і віртуальна пам'ять ?

2. Як зарезервувати фрагмент віртуальної пам'яті ?

3. Чи можна виділити блок реальної пам'яті, якщо не резервувати

віртуальну пам'ять ?

4. Як виділити реальну пам'ять ?

5. Як перемістити блок виділеної пам'яті ?

6. Що таке дефрагментація пам'яті ?

7. Як звільнити блок пам'яті ?

8. Які ви знаєте функції для роботи з файлами ?

9. Як створюються файли ?

10. Що означає "відкрити файл через оболонку" ?

11. Як працює функція GetOpenFileNameA ?

12. Як працюють функції GetFileTime та FileTimeToSystemTime ?

Page 171: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

Лабораторна робота № 4

Cтандартні класи вікон та їх типи

Мета роботи:

1. Ознайомитися з особливостями вікон, як таких об'єктів WINDOWS, які крім

текстово-графічної інформації можуть отримувати та надсилати спеціальні

структури даних, які називаються повідомленнями.

2. Перевірити принципи роботи програми, в якій створюється стандартне вікно

класу Button - кнопка, та утворюється цикл прийому повідомлень від цього вікна.

В циклі перевіряється подія натиснення клавіші <ESC>

Основні теоретичні відомості

Все, що бачить користувач на екрані в системі WINDOWS є вікном. Вікно - це

графічна оболонка, через яку програма може спілкуватися з користувачем.

Якщо програмі не потрібно спілкуватись, то вона може і не створювати вікон.

Вікно може володіти набором інших вікон, які називаються дочірніми.

Кожне вікно має як певний набір параметрів, так і може відрізнятися певною

специфікою спілкування з користувачем. Такі специфічні особливості

називаються класом вікна. Існують стандартні класи, наприклад, вікно-

регулювач, вікно-кнопка, вікно для вводу тексту, вікно-підказка та інші, які ви

знайдете в лабораторній роботі. Кожний стандартний клас має унікальну назву,

яка дійсна для всіх версій операційних систем WINDOWS.

Вікно створюється за допомогою спеціальної функції CreateWindowExA. В

параметрах цієї функції вказується наступна інформація:

Вказівник на MDI - структуру (або 0);

Хендл програми (отримується функцією GetModuleHandle);

Хендл меню або дочірнього вікна (або 0);

Хендл вікна-власника (або 0);

Висота вікна;

Ширина вікна;

Координата Y;

Координата X;

Прапорці стилю вікна (див. далі);

Вказівник на назву вікна (або 0);

Вказівник на назву класу (див. далі);

Прапорці властивостей вікна (див. далі).

Керувати вікном можна за допомогою внутрішнього механізму WINDOWS,

який базується на понятті повідомлення (в програмі позначено WM_...). Отже,

вікно - це такий об'єкт WINDOWS, який крім текстово-графічної інформації

може отримувати та надсилати спеціальні структури даних, які називаються

повідомленнями. Структура кожного повідомлення стандартна і складається з

наступних змінних:

Page 172: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

MsHWND dd 0 ; хендл вiкна, процедура якого отримала повiдомлення;

msMESSAGE dd 0 ; код повiдомлення (кожна подiя має свiй);

msWPARAM dd 0 ; додатковий параметр 1 (залежить вiд подiї);

msLPARAM dd 0 ; додатковий параметр 2 (залежить вiд подiї);

msTIME dd 0 ; час, коли було надiслано повiдомлення;

ptX dd 0 ; координата X миші, коли надсилалося повiдомлення;

ptY dd 0 ; координата Y миші, коли надсилалося повiдомлення.

Повідомлення сигналізує про деяку подію в системі або у вікні, наприклад,

вичерпався час таймера, користувач натиснув клавішу, відпустив клавішу,

порухав мишу, клацнув кнопкою, та інші.

Для того, щоб відправити повідомлення до довільного вікна (наприклад, щоб

змінити його розмір) необхідно заповнити цю структуру даних та

скористуватися функцією SendMessageA, а для прийому повідомлення від

певного вікна необхідно вказати діапазон прийому, хендл вікна та адресу

структури повідомлення і скористатися функцією GetMessageA. Коли

повідомлення надійде, операційна система сама заповнить всі дані структури

(див. програму).

З кожним стандартним класом вікна зв'язана певна віконна стандартна

процедура WndProc, яка малює вікно та обробляє всі повідомлення, що

надходять у вікно. Програміст має можливість вставити у віконну процедуру

свій фрагмент програми. Такі дії називаються субкласуванням.

Саме ідеєю субкласування можна пояснити відокремлення процесів створення

вікна, циклу прийому повідомлень та їх обробку у вигляді віконної процедури.

Для цього ж кожна віконна процедура отримує параметри через стек за

однаковим стандартом незалежно від класу та типу вікна. Цей стандарт не

залежить навіть від версії WINDOWS:

Hwnd // хендл вікна, яке отримало повідомлення;

UMsg // код повідомлення;

Wparam // перший параметр повідомлення;

Lparam // другий параметр повідомлення;

В лабораторній роботі запропонована програма, в якій створюється стандартне

вікно класу Button - кнопка, та утворюється цикл прийому повідомлень від

цього вікна. В циклі перевіряється подія натиснення клавіші <ESC>. Якщо

користувач натиснув <ESC>, то програма закінчується, інакше повідомлення

передається у стандартну віконну процедуру.

В програмі використана функція GetModuleHandleA, яка необхідна для того,

щоб прив'язати вікно до програмного модуля.

Текст програми

Page 173: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

.386

.model flat,STDCALL

extrn InitCommonControls:Proc

extrn GetModuleHandleA:Proc

extrn ExitProcess:Proc

extrn CreateWindowExA:Proc

extrn GetMessageA:Proc

extrn DispatchMessageA:Proc

extrn TranslateMessage:Proc

; Визначення типів (стилів) вікон:

WS_POPUP equ 80000000h

WS_VISIBLE equ 010000000h

WS_DLGFRAME equ 400000h

WS_EX_TOPMOST equ 8h

; Код повідомлення про натиснуту клавішу:

WM_KEYDOWN equ 100h

.data

MSG: ; Структура стандартного повідомлення WINDOWS

msHWND dd 0 ; хто надіслав

msMESSAGE dd 0 ; яку інформацію

msWPARAM dd 0 ; про що

msLPARAM dd 0 ; і як

msTIME dd 0 ; коли

ptX dd 0 ; де (X),

ptY dd 0 ; (Y)

AppHWnd dd 0

NewHWnd dd 0

WindowCaption db 'стандартний клас вікна',0

CLASSNAME db 'Button',0 ; Ім'я стандартного класу

;=======================================

.code

Start: call InitCommonControls

push 0h

call GetModuleHandleA

mov AppHWnd,eax

push 0 ; MDI-структура

push AppHWnd ; програмний модуль

push 0 ; меню

push 0 ; власник

push 100 ; висота

Page 174: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

push 160 ; ширина

push 250 ; координата y

push 300 ; координата x

push WS_POPUP or WS_VISIBLE or 4 or 1

push offset WindowCaption ; Заголовок вікна

push offset CLASSNAME ; Ім'я класу вікна

push WS_EX_TOPMOST ; Властивості

call CreateWindowExA ; Створити вікно

mov NewHWnd,eax ; Зберегти хендл вікна

;=======================================

MSG_LOOP: ; Цикл прийому повідомлень

push 0 ; діапазон прийому -

push 0 ; всі можливі повідомлення

push NewHWnd ; від даного вікна

push offset MSG ; Адреса структури повідомлення

call GetMessageA

cmp msMESSAGE,WM_KEYDOWN

jnz CONTINUE_LOOP

cmp msWPARAM,1bh ; код <ESC>

jz STOP

CONTINUE_LOOP:

push offset MSG

call TranslateMessage

push offset MSG ; Направити повідомлення у ві-

call DispatchMessageA ; конну процедуру.

jmp MSG_LOOP

;==============================

STOP:

push 0

call ExitProcess

End Start

ПОРЯДОК ВИКОНАННЯ РОБОТИ

1. Відкомпілювати подану програму.

2. Замість стандартного класу Button ввести наступні назви класів:

ButtonListBox, ComboBox, Edit, ListBox, Message, ScrollBar, Static,

SysAnimate32, SysHeader32, SysListView32, SysTabControl32,

SysTreeView32, TTSubclass, ToolTips, ToolbarWindow32, msctls_hotkey32,

msctls_progress32, msctls_statusbar32, msctls_trackbar32, msctls_updown32,

tooltips_class32, #32768, #32769, #32770, #32771.

3. Описати вікна кожного класу та їх властивості.

Зміст звіту

Page 175: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

1. Мета роботи.

2. Короткі теоретичні відомості і розрахункові формули.

3. Вихідні дані для виконання роботи (відповідно до отриманого

варіанта).

4. Результати виконання роботи, отримані при різних вихідних даних.

5. Контрольні запитання

6. Висновки.

КОНТРОЛЬНІ ЗАПИТАННЯ

1. Що таке вікно і як його створити ?

2. Які параметри потрібно вказати щоб створити вікно ?

3. Що таке віконна процедура і які параметри до неї передаються ?

4. Що таке клас вікна та субкласування ?

5. Що таке повідомлення і яка його структура?

6. Для чого надсилаються повідомлення ?

7. Які існують стандартні класи вікон ?

8. Що таке тип (стиль) вікна ?

Лабораторна робота № 5

Дочірні вікна: їх утворення та взаємодія, графічний контекст.

Мета роботи:

1. Ознайомитися з особливостями дочірніх вікон, як це вікон типу

"WS_CHILD", які існують для відображення допоміжних органів керування або

довідки.

2. Перевірити принципи роботи функції MessageBoxA, GetComputerNameA,

GetCurrentDirectoryA, GetDriveTypeA, GetSystemDirectoryA, GetTempPathA,

GetUserNameA, GetWindowsDirectoryA та взаємодію між дочірнім та основним

вікном

Основні теоретичні відомості

Дочірні вікна - це вікна типу "WS_CHILD", які існують для відображення

допоміжних органів керування або довідки. Основною властивістю цих вікон є

те, що вони не мають меню і не можуть бути спливаючими вікнами

(WS_POPUP). Дочірні вікна, як і всі вікна, створюються функціями API, що

мають в назві морфологічний корінь "CreateWindow". При створенні дочірнього

вікна в параметрах обов'язково вказується його власник а також стиль

"WS_CHILD". Параметр, який містить для звичайних вікон хендл меню,

визначає ідентифікатор дочірнього вікна, який передається у батьківське вікно як

параметр повідомлення "WM_NOTIFY". Це повідомлення є основним для

дочірніх вікон, і завжди передається у батьківське вікно, якщо у дочірньому

виникають будь-які події. Крім цього, кожний тип дочірнього вікна має свій

власний набір повідомлень, які дають додаткову інформацію про ці події.

Керувати дочірніми вікнами можна так само, як і звичайними вікнами,

наприклад, посилаючи до них повідомлення функцією SendMessageA. У поданій

Page 176: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

нижче програмі дочірньому вікну класу msctls_trackbar32 після його створення

надсилається повідомлення TBM_SETRANGE для встановлення кількості

поділок до 255 (знаходиться у старшому слові параметра повідомлення).

Кожне вікно, в тому числі і дочірнє, має спеціальну керуючу структуру даних,

яка називається графічним контекстом. Вона відповідає за графічні властивості

вікна, наприклад, при виводі тексту, ліній чи фону. Для отримання графічного

контексту вікна використовується функція GetDC. Ця функція нагадує відкриття

файлу: поки файл відкритий, змінити його може лише той процес, що

знаходиться в даному кодовому сегменті.

Кожна графічна операція використовує графічний контекст як параметр. Драйвер

принтера також забезпечує графічний контекст, тому ті самі графічні операції

можуть виводити дані або у вікно, або на принтер. Графічний контекст містить

також інформацію про шрифт, з яким працюють графічні функції (в даній

програмі - функція TextOutA ).

Наступну програму необхідно запустити щоб краще зрозуміти взаємодію між

дочірнім та основним вікном. Перед тим, як копіювати її уважно прогляньте

коментарі.

Текст програми

.386

.model flat,STDCALL

extrn InitCommonControls:Proc, GetModuleHandleA:Proc, ExitProcess:Proc

extrn CreateWindowExA:Proc, RegisterClassA:Proc, GetMessageA:Proc

extrn DispatchMessageA:Proc, DefWindowProcA:Proc, Proc,_wsprintfA:Proc

extrn GetDC:Proc, SelectObject:Proc, ReleaseDC:Proc, CreateSolidBrush:Proc

extrn Rectangle:Proc, TextOutA:Proc,SendMessageA:Proc, lstrlen:Proc

WS_CHILD EQU 40000000h

WS_POPUP EQU 80000000h

WS_VISIBLE EQU 010000000h

WS_DLGFRAME EQU 400000h

WM_KEYDOWN EQU 100h

TBS_TOP EQU 0004h

WM_NOTIFY EQU 4Eh

WM_HSCROLL EQU 114h

TBM_SETRANGE EQU 1030

;=====================================================

.data

WC dd 4003h,offset WndProc,5 dup(0),1,0,offset WndClassName

msg dd 0

msMESSAGE dd 0

msWPARAM dd 0,0,0,0,0

AppHWnd dd 0

MainHWnd dd 0

CHILD_CLASS_NAME db 'msctls_trackbar32',0

Page 177: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

WndClassName db "ABBA",0

COLOR dd 0

Brush dd 0

HDC dd 0

TRACK_ID dd 0

TRACK_ID1 dd 0

FORMAT db 'Колiр = %X',0

PRINT_BUF db 20 dup(0)

;=======================================================

.code

Start: call GetModuleHandleA,0 ; отримати хендл програми для створення вікна;

mov AppHWnd,eax

call RegisterClassA,offset WC ; зареєструвати новий клас вікон;

call CreateWindowExA,0,eax,0,WS_POPUP or WS_VISIBLE or WS_DLGFRAME,

100,50,400,410,0,0,AppHWnd,0

mov MainHWnd,eax

call CreateWindowExA,0,offset CHILD_CLASS_NAME,0,WS_CHILD or

WS_VISIBLE or TBS_TOP, 5,360,380,35,MainHWnd,0,AppHWnd,0

call SendMessageA,eax,TBM_SETRANGE,1,255*10000h

call CreateWindowExA,0,offset CHILD_CLASS_NAME,0,WS_CHILD or

WS_VISIBLE or TBS_TOP, 5,320,380,35,MainHWnd,1,AppHWnd,0

call SendMessageA,eax,TBM_SETRANGE,1,255*10000h

;==========================================================

msg_loop: call GetMessageA,offset msg,MainHWnd,0,0

cmp msMESSAGE,WM_KEYDOWN ; Повідомлення клавіатури

jnz CONTINUE_LOOP

cmp msWPARAM,1bh ; якщо код <ESC>, то STOP

jz STOP

CONTINUE_LOOP:

Call DispatchMessageA,offset msg

Jmp msg_loop

STOP: call ExitProcess,0

;==========================================================

WndProc proc hwnd:DWORD, wmsg:DWORD, wparam:DWORD, lparam:DWORD

Cmp wmsg,WM_NOTIFY ; WM_NOTIFY надходить від кожного

Jnz NO_NOTIFY ; дочірнього вікна, при тому

Mov eax,wparam ; wparam містить ідентифікатор

Xchg eax,TRACK_ID1 ; дочірнього вікна від якого надійшло

Xchg eax,TRACK_ID ; повідомлення, який записуємо у TRACK_ID;

NO_NOTIFY: cmp wmsg,WM_HSCROLL ; WM_HSCROLL надходить від

Jnz NO_CHILD ; вікон типу "горизонтальний скролінг"

Mov eax,wparam ; молодша частина wparam - дія,

Shr eax,16 ; старша частина - позиція ковзуна;

Jz NO_CHILD

Mov ebx,TRACK_ID ; для захисту від "глюків" даний та

Cmp ebx,TRACK_ID1 ; попередній ідентифікатор дочірнього

Page 178: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

Jnz NO_CHILD ; вікна повинні співпадати;

Add ebx,offset COLOR ; ідентифікатор використовується

Mov [ebx],al ; як індекс кольору;

Call CreateSolidBrush,COLOR ; створюється кисть кольору

Mov Brush,eax

Call GetDC, hwnd ; одержуємо графічний контекст;

Mov HDC,eax

Call SelectObject,HDC,Brush ; змінюємо поточну кисть;

Call Rectangle,HDC,0,0,400,270 ; малюємо прямокутник

Call _wsprintfA, offset PRINT_BUF,offset FORMAT,COLOR

Add esp,4*3

Call lstrlen,offset PRINT_BUF ; отримуємо довжину рядка

Call TextOutA, HDC, 10, 250, offset PRINT_BUF, eax

Call ReleaseDC,hwnd,HDC ; звільняємо графічний контекст

NO_CHILD:

Call DefWindowProcA,hwnd,wmsg,wparam,lparam

ret

WndProc endp

End Start

Переходимо до вибору шрифтів у графічному контексті. Для цього необхідно

створити гарнітуру відповідного фонту заданого розміру. Така гарнітура

створюється функцією CreateFontIndirectA. Коли створяться зображення букв,

функції виводу тексту будуть працювати дуже швидко. Необхідно лише

записати утворену гарнітуру шрифту у графічний контекст вікна функцією

SelectObject.

Текст можна малювати різними кольорами, які утворюються сумішшю

компонент червоного (1й байт), зеленого (2-й байт) та синього (3-й байт) в одній

змінній, яка вказуються як параметр функції SetTextColor. Аналогічно

вибирається код фону функцією SetBkColor. Після виводу букв заданої

гарнітури функцією TextOutA, графічний контекст необхідно звільнити.

В нижчеподаному прикладі фразу "Програмування на Асемблерi" необхідно

подати у кодуванні DOS. Для цього в програмі Far.exe натисніть клавішу F8 щоб

знизу у 8-й позиції підказки з'явилося "Win", після чого скопіюйте текст у

редакторі Far.exe.

Текст програми

.386

.model flat,stdcall

Extrn CreateFontIndirectA:Proc, GetDC:Proc, SelectObject:Proc, SetTextColor:Proc

Extrn SetBkColor:Proc, TextOutA:Proc, ReleaseDC:Proc, ExitProcess:Proc

.data

HDC dd 0

Page 179: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

LOGFONTA:

lfHeight DD 70 ; Висота

lfWidth DD 20 ; Ширина

lfEscapement DD 3600-100 ; Кут нахилу * 10

lfOrientation DD 0 ;

lfWeight DD 1000 ; Жирнiсть (1000-Bold)

lfItalic DB 1 ; Курсив

lfUnderline DB 0 ; Пiдкреслення

lfStrikeOut DB 0

lfCharSet DB 255

lfOutPrecision DB 0

lfClipPrecision DB 0

lfQuality DB 1

lfPitchAndFamily DB 0

lfFaceName DB 'Impact' ; Назва фонту

Reserv DB 32-6 dup(0)

X_coord dd 80

Y_coord dd 80

Text1 db 'Програмування на Асемблерi '

EndText1 db 0

.code

Start: call GetDC,0

mov HDC,eax

call CreateFontIndirectA, offset LOGFONTA

call SelectObject,HDC,eax

call SetTextColor,HDC,0022FFh

call SetBkColor,HDC,00FFFFh

mov eax,offset EndText1

sub eax,offset Text1

call TextOutA, HDC, X_coord,Y_coord, offset Text1, eax

call ReleaseDC,0,HDC

call ExitProcess,0

End Start

ПОРЯДОК ВИКОНАННЯ РОБОТИ

1. Відкомпілювати та запустити подані програми.

2. Переробити першу програму таким чином, щоб вона мала три

дочірніх вікна класу msctls_trackbar32, кожне з яких регулюватиме один з

кольорів: червоний, зелений, синій.

3. Переробити другу програму так, щоб текст знаходився у вікні. За

основу взяти першу програму.

4. Ввести в програму дочірнє вікно класу SysAnimate32 з стилем

ACS_AUTOPLAY. Після створення вікна цього стилю надіслати йому

Page 180: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

повідомлення ACM_OPEN та ACM_PLAY. Формат повідомлень знайти в

Win32.hlp.

5. Ввести в програму дочірнє вікно класу Edit з стилями ES_CENTER

та ES_MULTILINE. За допомогою повідомлення EM_GETLINE

переписати в буфер введену інформацію та вивести її за допомогою

функції MessageBoxA. Для коректної роботи програми з вікном класу Edit

в цикл прийому повідомлень вставити функцію TranslateMessage.

6. Створити в основній програмі таймер за допомогою функції

SetTimer, який буде щосекунди надсилати повідомлення WM_TIMER у

процедуру основного вікна. З приходом кожного такого повідомлення у

процедурі вікна збільшувати змінну-лічильник і надсилати її текстове

значення у дочірнє вікно класу Edit за допомогою повідомлення

WM_SETTEXT.

Зміст звіту

1. Мета роботи.

2. Короткі теоретичні відомості і розрахункові формули.

3. Вихідні дані для виконання роботи (відповідно до отриманого

варіанта).

4. Результати виконання роботи, отримані при різних вихідних даних.

5. Контрольні запитання

6. Висновки.

КОНТРОЛЬНІ ЗАПИТАННЯ

1. Що таке дочірнє вікно ?

2. Як отримати інформацію від дочірнього вікна ?

3. Як керувати дочірнім вікном ?

4. Що таке графічний контекст ?

5. Що таке шрифт і для чого існує функція CreateFontIndirectA ?

Лабораторна робота № 6

Спеціалізовані каталоги WINDOWS

Мета роботи:

1. Ознайомитися з особливостями операційної системи, як середовищем для

"співіснування" багатьох програм, які взаємодіють між собою та використовують

спільні ресурси

2. Перевірити способи уникнути протиріч між програмами та зберігати

настройки системиз використанням спеціальної бази даних, що називається

реєстром і зберігається у файлах user.dat та system.dat.

Основні теоретичні відомості

Операційна система - це середовище для "співіснування" багатьох програм, які

взаємодіють між собою та використовують спільні ресурси. Для того, щоб

уникнути протиріч між програмами та зберігати настройки системи, розробники

WINDOWS ввели спеціальну базу даних, що називається реєстром. Реєстр

Page 181: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

зберігається у файлах user.dat та system.dat. У половині випадків крах

операційної системи пов'язаний з невірною інформацією саме в цих файлах.

Реєстр нагадує дерево каталогів, кожний з яких називається ключем. Кожна

програма, що користується спільними системними ресурсами робить записи в

реєстр які саме ресурси вона використовує і які початкові параметри давати при

старті.

Крім того, в операційній системі WINDOWS існують спеціальні каталоги, в

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

вигляді меню. За кожним таким спеціалізованим каталогом закріплений певний

внутрішній індекс, який позначається певною стандартною константою. Нижче

наведені деякі з цих констант, їх значення та зміст.

Константа Значен

ня Коментарі

CSIDL_DESKT

OP 0

Програми, що знаходяться на екрані -

"Рабочий стол" (Desktop)

CSIDL_PROGR

AMS 2

Пункт системного меню "Программы"

(Program files)

CSIDL_PRINT

ERS 4 Містить встановлені принтери

CSIDL_PERSO

NAL 5

Папка "Мои документы" (My

Documents)

CSIDL_FAVOR

ITES 6 Папка "Избранное" (Favorites)

CSIDL_START

UP 7

Програми, що стартують автоматично

"Автозагрузка" (Startup)

CSIDL_RECEN

T 8

Системне меню "Документы"

(Documents)

CSIDL_SENDT

O 9

Вміст пункту меню "Отправить"

(SendTo)

CSIDL_START

MENU 0bh

Вміст розділу "Главное меню"

(StartMenu)

CSIDL_NETHO

OD 13h

Розділ "Сетевое окружение" (Network

neighborhood)

CSIDL_FONTS 14h Папка зі шрифтами WINDOWS (Fonts)

CSIDL_TEMPL

ATES 15h Шаблони документів (Templates)

CSIDL_APPDA

TA 1ah Робочі папки встановлених програм

Page 182: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

CSIDL_INTER

NET 20h Тимчасові INTERNET -файли

CSIDL_COOKI

ES 21h

Файли персоналізації доступу в

INTERNET

CSIDL_HISTO

RY 22h Звіт про роботу користувача

Користувач отримує назву спеціалізованого каталогу за 2 кроки:

1. За допомогою функції SHGetSpecialFolderLocation знаходить

дескриптор (хендл) спеціалізованої папки, який залежить від індексу, що

знаходиться в таблиці;

2. За допомогою функції SHGetPathFromIDList по хендлу знаходить

назву самої папки.

Нижче наведена програма, що демонструє ці кроки і знаходить всі

спеціалізовані каталоги:

Текст програми

.386

.model flat,STDCALL

extrn SHGetSpecialFolderLocation:Proc

extrn MessageBoxA:Proc

extrn ExitProcess:Proc

extrn SHGetPathFromIDList:Proc

.data

INDEX dd 0 ; індекс папки;

DIRECTORY_ID dd 0 ; хендл папки;

DIRECTORY_NAME db 512 dup(0) ; ім'я папки;

T db ' Cпеціальний каталог:',0

.code

Start: push offset DIRECTORY_ID ; вихідний параметр;

push INDEX ; вхідний номер папки;

push 0 ; хендл власника;

call SHGetSpecialFolderLocation

inc INDEX

cmp INDEX,65 ; якщо індекс перевищує 64,

jnc STOP ; то закінчити пошук.

Page 183: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

or eax,eax ; Якщо папка з даним індексом не існує,

jnz Start ; то продовжувати пошук.

push offset DIRECTORY_NAME ; Адреса вихідного імені;

push DIRECTORY_ID ; вхідний хендл папки.

call SHGetPathFromIDList

; Вивід отриманої назви каталогу

call MessageBoxA,0,offset DIRECTORY_NAME,offset T,0

jmp Start

STOP: call ExitProcess,0

end Start

ПОРЯДОК ВИКОНАННЯ РОБОТИ

1. Відкомпілювати та запустити подану програму.

2. Переробити програму таким чином, щоб вона крім назви каталогу

видавала значення його індексу та хендлу.

3. Використовуючи дану програму та функцію CopyFileA, написати

програму для копіювання тексту програми на Робочий стіл.

4. Написати програму для встановлення картинки робочого стола з

файлу Wallpaper.bmp, що знаходиться в папці "Избранное". Для цього

отримати ім'я спеціалізованого каталогу, приєднати до нього символ "\",

назву файлу та за допомогою функції SystemParametersInfoA встановити

нову картинку робочого стола.

Довідкова інформація по функціях API, що використовуються в роботі

SystemParametersInfoA - змінює системні параметри та профіль користувача:

uiAction // номер параметру, який необхідно змінити

(SPI_SETDESKWALLPAPER);

uiParam // додаткова інформація про параметр (NULL);

pvParam // уточнююча інформація (адреса рядка з повною назвою файлу);

fWinIni // режим виконання (SPIF_SENDWININICHANGE +

SPIF_UPDATEINIFILE).

lstrcat - з'єднує два рядка в один (кінець рядка позначено нулем):

lpString1 // адреса рядка до якого треба приєднати;

lpString2 // адреса рядка, що приєднується.

Зміст звіту

1. Мета роботи.

2. Короткі теоретичні відомості і розрахункові формули.

3. Вихідні дані для виконання роботи (відповідно до отриманого

варіанта).

4. Результати виконання роботи, отримані при різних вихідних даних.

Page 184: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

5. Контрольні запитання

6. Висновки.

КОНТРОЛЬНІ ЗАПИТАННЯ

1. Що таке спеціалізований каталог WINDOWS ?

2. Чим реєстр відрізняється від спеціалізованого каталогу ?

3. Які ви знаєте спеціалізовані каталоги ?

4. Як отримати назву спеціалізованого каталогу ?

5. Що робить функція SystemParametersInfoA ?

Лабораторна робота № 7

Зворотній виклик та функції перебору системних об'єктів

Мета роботи:

1. Ознайомитися з особливостями процедури зворотного виклику, яка отримує

хендл об'єкта як параметр, тобто одержує до нього доступ. В цій процедурі

користувач може використовувати додаткові функції для отримання інформації

про об'єкт, або змінити параметри самого об'єкту, пославши до нього відповідне

повідомлення.

2. Перевірити принципи роботи програми використання функції для отримання

всіх підключів відкритого ключа реєстру "Software\Microsoft\Internet Explorer", в

якому не використовується callback-процедура, а підключі знаходяться по

індексу

Основні теоретичні відомості

В операційній системі WINDOWS існують спеціальні функції, які дозволяють

робити перебір деяких системних об'єктів, наприклад, вікон, ключів реєстру,

мережних з'єднань і т.д. В назви таких функцій включається слово "Enum" (від

англійського слова enumerate-перелічувати). В процесі роботи таких функцій

система знаходить хендл об'єкту, що підлягає перебору (переліку) та передає

його у спеціальну процедуру зворотного виклику, яка має бути написана

користувачем. Таке звернення до системної функції, яка в свою чергу викликає

підпрограму користувача, називається технологією зворотного виклику (від

англійського слова callback). Отже, процедура зворотного виклику отримує

хендл об'єкта як параметр, тобто одержує до нього доступ. В цій процедурі

користувач може використовувати додаткові функції для отримання інформації

про об'єкт, або змінити параметри самого об'єкту, пославши до нього відповідне

повідомлення.

Деякі функції нумерації не використовують зворотного виклику. Такі функції

вимагають звертатися до об'єкта по індексу, подібно до того, як було зроблено в

попередній лабораторній роботі. В наступній таблиці представлені деякі функції

переліку та їх зміст.

Page 185: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

Функція Функція знаходить:

EnumWindows всі вікна верхнього рівня в системі

RegEnumKeyExA всі підключі (subkeys) відкритого ключа

реєстру

RegEnumValueA всі значення відкритого ключа реєстру

EnumResourceTyp

esA всі типи ресурсів даної програми

WnetEnumResourc

eA

всі мережні ресурси, що доступні

користувачеві

NetConnectionEnu

m всі мережні з'єднання користувача

NetFileEnum всі відкриті файли на сервері

NetGroupEnum всі групи користувачів

NetScheduleJobEn

um

всі завдання, що виконуються на любому

комп'ютері

NetServerDiskEnu

m всі диски на віддаленому комп'ютері

NetServerEnum всі сервери певного типу на віддаленому

комп'ютері

NetUserEnum всі рахунки користувачів на сервері

NetWkstaUserEnu

m

всі імена користувачів, що працюють за

комп'ютерами

RasEnumConnectio

nsA

всі з'єднання віддаленого доступу до

комп'ютера

EnumProtocolsA всі встановлені протоколи передачі даних

EnumPrintersA всі локальні та віддалені принтери

EnumEnhMetaFile всі типи даних у заданому метафайлі

EnumFontsA всі шрифти на певному пристрою та в

системі

EnumObjects всі об'єкти, що пов'язані з графічним

контекстом вікна

AcmDriverEnum всі драйвери для аудіо компресії

AcmFormatEnumA всі формати файлів для даного драйвера

EnumChildWindo

ws всі дочірні вікна для заданого вікна

EnumClipboardFor

mats

всі типи даних, що знаходяться в буфері

обміну

Page 186: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

EnumDesktopWind

ows всі вікна Робочого стола

EnumDesktopsA всі робочі столи, які є на комп'ютері

EnumDisplaySettin

gsA всі режими роботи дисплея

EnumThreadWindo

ws всі батьківські вікна заданого процесу

EnumWindowStati

onsA всі робочі станції на даному комп'ютері

Нижче наведений приклад для переліку всіх відкритих вікон вищого рівня в

операційній системі:

Текст програми

.386

.model flat,STDCALL

extrn ExitProcess:Proc

extrn MessageBoxA:Proc

extrn EnumWindows:Proc

extrn GetWindowTextA:Proc

.data

TITLE1 db 'Перелiк вiкон',0

WND_NAME db 200 dup(0)

;===============================

.code

Start: push 0 ; додатковий параметр, що передається до CallBack - функції;

push offset PROG1 ; адреса CallBack - функції;

call EnumWindows

STOP: call ExitProcess,0

;================================

PROG1 proc hwnd:DWORD, wparam:DWORD ; CallBack - процедура;

push 200 ; максимальна довжина назви;

push offset WND_NAME ; адреса назви вікна;

push hwnd ; хендл вікна;

call GetWindowTextA ; отримати назву вікна;

call MessageBoxA,0,offset WND_NAME,offset TITLE1,30h ; вивести назву;

or eax,1 ; 1 - шукати далі, 0 - закінчити перебір.

Ret ; Повернутися до процедури EnumWindows.

Page 187: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

Endp PROG1

End Start

В даному прикладі використана callback-процедура, яка отримує і виводить

назву вікна. Кожна callback-процедура повинна повернути ненульове значення в

регістрі eax, інакше перелік припиниться.

В наступній програмі наведений приклад використання функції для отримання

всіх підключів відкритого ключа реєстру "Software\Microsoft\Internet Explorer", в

якому не використовується callback-процедура, а підключі знаходяться по

індексу:

Текст програми

.386

.model flat,STDCALL

extrn ExitProcess:Proc

extrn RegOpenKeyA: Proc

extrn RegEnumKeyExA:Proc

extrn MessageBoxA:Proc

HKEY_CURRENT_USER equ 80000001h

.data

SizeKeyClassName dd 37 ; довжина назви ключа

KeyClassName db 'Software\Microsoft\Internet Explorer',0

SizeSubKeyName dd 260

SubKeyName db 260 dup(0)

IndexKey dd 0 ; індекс ключа;

KeyHandle dd 0 ; хендл відкритого ключа;

KeyTime dq 0,0,0,0

;===========================================

.code

Start: push offset KeyHandle ; адреса, де буде записаний хендл ключа;

push offset KeyClassName ; назва ключа;

push HKEY_CURRENT_USER ; стандартний розділ реєстру;

call RegOpenKeyA ; отримати хендл ключа;

or eax,eax

jnz STOP ; перейти на кінець, якщо ключ не існує.

mov eax,260 ; Встановити максимальний розмір для назви підключа.

mov SizeSubKeyName,eax

mov eax,37 ; Встановити розмір для назви ключа.

mov SizeKeyClassName,eax

push offset KeyTime ; адреса часу створення та останньої зміни ключа;

push offset SizeKeyClassName ; адреса розміру імені класу,

Page 188: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

push offset KeyClassName ; адреса назви класу ключів реєстру;

push 0 ; зарезервовано;

push offset SizeSubKeyName ; адреса розміру імені підключа,

push offset SubKeyName ; адреса назви піключа реєстру;

push IndexKey ; індекс підключа,

push KeyHandle ; хендл підключа.

call RegEnumKeyExA ; Отримати всі підключі;

cmp eax,0 ; якщо eax=0, то успішне виконання.

jnz STOP

call MessageBoxA,0,offset SubKeyName,offset KeyClassName,1 ; вивести;

inc IndexKey ; збільшити індекс підключа.

cmp eax,1 ; якщо натиснули "OK",

jz Start ; то перейти на Start.

STOP: call ExitProcess,0

End Start

ПОРЯДОК ВИКОНАННЯ РОБОТИ

1. Відкомпілювати та запустити подані програми.

2. Переробити першу програму таким чином, щоб вона видавала запит

на закриття певного вікна у формі вікна повідомлень з двома кнопками

"YES" та "NO", і якщо користувач натисне "YES", вікну надсилається

повідомлення про закриття за допомогою функції SendMessageA з

параметром WM_CLOSE (див. довідкову інформацію та файл

H:\TASM\INCLUDE\Win119.inc).

3. Переробити другу програму таким чином, щоб вона видавала всі

назви атрибутів та їх значення для підключа реєстру

"Software\Microsoft\Internet Explorer\Main" за допомогою функції

RegEnumValueA (див. довідкову інформацію).

Довідкова інформація по функціях API, що використовуються в роботі

SendMessageA - надсилає повідомлення вікну або пристрою:

hWnd // хендл вікна або пристрою, якому ви надсилаєте повідомлення;

Msg // код повідомлення (що треба зробити у вікні);

wParam // параметр 1;

lParam // параметр 2;

RegEnumValueA - отримує всі атрибути та їх значення для відкритого ключа

реєстру:

hKey, // хендл відкритого ключа реєстру;

dwIndex, // індекс атрибуту, інформацію про який ви отримуєте;

lpValueName, // адреса буферу для назви атрибуту;

lpcbValueName // адреса розміру цього буферу;

Page 189: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

lpReserved // зарезервовано = 0;

lpType // адреса змінної типу атрибуту;

lpData // адреса буферу, в який запишеться значення атрибуту;

lpcbData // адреса довжини цього буферу;

Зміст звіту

1. Мета роботи.

2. Короткі теоретичні відомості і розрахункові формули.

3. Вихідні дані для виконання роботи (відповідно до отриманого

варіанта).

4. Результати виконання роботи, отримані при різних вихідних даних.

5. Контрольні запитання

6. Висновки.

КОНТРОЛЬНІ ЗАПИТАННЯ

1. Як працюють функції переліку системних об'єктів ?

2. Які ви знаєте API-функції переліку ?

3. Як отримати назву вікна ?

4. Яка структура системного реєстру ?

5. Як отримати назви та значення атрибутів системного реєстру ?

Лабораторна робота № 8

Використання технології OLE

Мета роботи:

1. Ознайомитися з особливостями OLE (object linking and embedding), яка

полягає в систематизації структур даних та функцій роботи з ними. Такий підхід,

де кожний об'єкт має в собі інформацію про себе називається об'єктно-

орієнтованим програмуванням..

2. Перевірити принципи роботи інтерфейсів об'єктно-орієнтованого

програмування: IStream та IPicture

Основні теоретичні відомості

Основна ідея OLE (object linking and embedding) полягає в систематизації структур даних та

функцій роботи з ними. Такий підхід, де кожний об'єкт має в собі інформацію про себе

називається об'єктно-орієнтованим програмуванням. Для роботи з об'єктами у WINDOWS

введено поняття інтерфейс. Інтерфейси для роботи з об'єктами можна додавати в операційну

систему. Але існують і стандартні інтерфейси для роботи зі стандартними об'єктами. Кожний

стандартний інтерфейс має своє ім'я і обліковий код, який прописаний у реєстрі під ключем

HKEY_CLASSES_ROOT\Interface\. Кожен підключ даного ключа є кодом інтерфейсу, а

значення підключа є ім'ям інтерфейсу. Наприклад, підключ {7BF80980-BF32-101A-8BBB-

00AA00300CAB} має значення IPicture.

Для використання бібліотеки OLE необхідно створити об'єкт типу "потік" (Stream), який

представляє данні, що завантажені в нього у структурованому вигляді. Теоретично потік даних

може бути необмеженим. Створення такого об'єкту виконує функція CreateStreamOnHGlobal,

яка знаходиться у бібліотеці ole32.dll. Початковим станом цього об'єкту є вміст віртуальної

Page 190: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

пам'яті, блок якої передається як параметр функції. Оскільки опису бібліотеки ole32.dll немає

у файлі import32.lib, програма tlink32.exe не зможе підключити виклик цієї функції, і ми

повинні будемо робити це вручну. Для цього скористуємось комбінацією двох функцій:

LoadLibraryA та GetProcAddress. Перша з них завантажує потрібну бібліотеку у пам'ять, а

друга - отримує адресу потрібної функції API.

В даній лабораторній роботі використано два інтерфейси об'єктно-орієнтованого

програмування: IStream та IPicture.

ПОРЯДОК ВИКОНАННЯ РОБОТИ

1. Створити файл для компіляції програм для 32х-бітного режиму:

2. Набрати подану нижче програму

3. Відкомпілювати набрану програму за допомогою командного рядка

4. Запустити одержаний файл на виконання. Записати ім'я комп'ютера, яке

отримала програма.

5. Розглянути текст програми, вивчити загальну структуру програми із

застосуванням функцій Win32 API. Переписати в зошит текст програми з

відповідними коментарями.

6. Замінити виклик функції API GetComputerNameA на виклик однієї з

наступних функцій, змінивши відповідним чином параметри:

GetUserNameA, GetWindowsDirectoryA, GetSystemDirectoryA, GetTempPathA,

GetCurrentDirectoryA, GetDriveTypeA, після чого відкомпілювати і запустити

програму.

Текст програми

.386

.model flat,STDCALL

extrn LoadLibraryA:Proc, GetProcAddress:Proc, GlobalAlloc:Proc, GlobalLock:Proc

extrn GlobalUnlock:Proc, GetDeviceCaps:Proc, GetClientRect:Proc, ReadFile:Proc

extrn CreateFileA:Proc, GetFileSize:Proc, CloseHandle:Proc, MulDiv:Proc

extrn GetDC:Proc, ReleaseDC:Proc, ExitProcess: Proc

.data

HWnd dd 0

HDC dd 0

HMWidth dd 0

Width dd 0

HMHeight dd 0

Height dd 0

RECT dd 0,0,0,0 ;left,top,right,bottom

PICTURE_NAME db 'test.jpg',0

Page 191: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

HFile dd 0

File_Size dd 0

HGlobal dd 0

NUM_READ dd 0

PSTM dd 0

; iдентифiкатор iнтерфейсу IPicture, який взятий з реєстру:

IID_IPicture dd 7bf80980h ; HKEY_CLASSES_ROOT\Interface\

dw 0bf32h, 101ah ; {7BF80980-BF32-101A-8BBB-00AA00300CAB}

db 8bh, 0bbh

db 00h, 0aah, 00h, 30h, 0ch, 0abh

GPICTURE dd 0

LibName1 db 'ole32.dll',0

ProcName1 db 'CreateStreamOnHGlobal',0

CreateStreamOnHGlobal1 dd offset STOP

LibName2 db 'oleaut32.dll',0

ProcName2 db 'OleLoadPicture',0

OleLoadPicture1 dd offset STOP

.code

Start: call LoadLibraryA,offset LibName1

call GetProcAddress, eax, offset ProcName1

or eax,eax

jz STOP

mov CreateStreamOnHGlobal1,eax

call LoadLibraryA,offset LibName2

call GetProcAddress, eax, offset ProcName2

or eax,eax

jz STOP

mov OleLoadPicture1,eax

call CreateFileA, offset PICTURE_NAME, 80000000h,0,0,3,0,0

cmp eax,-1

jz STOP

mov HFile,eax

call GetFileSize,HFile,0 ; отримати довжину файлу

mov File_Size,eax

call GlobalAlloc, 2, File_Size

mov HGlobal,eax

call GlobalLock,eax

call ReadFile, HFile, eax, File_Size, offset NUM_READ,0

call GlobalUnlock, HGlobal

call CloseHandle, HFile

call [CreateStreamOnHGlobal1], HGlobal, 1, offset PSTM

mov eax,PSTM ; завантажити файл у потiк

or eax,eax

jz STOP

call [OleLoadPicture1],PSTM,File_Size,0,offset IID_IPicture,offset GPICTURE

or eax,eax ; скористатись інтерфейсом об'єкт IPicture

jnz STOP

Page 192: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

mov edx,PSTM ; coinvoke PSTM,IUnknown,Release

mov edx,[edx]

push PSTM

call dword ptr [edx+8h]

mov edx, GPICTURE ; використання методу OLE

mov edx, [edx] ; IPicture::get_Width (18h)

push offset HMWidth

push GPICTURE

call Dword ptr [edx+18h]

mov edx, GPICTURE ; використання методу OLE

mov edx, [edx] ; IPicture::get_Height (1Ch)

push offset HMHeight

push GPICTURE

call Dword ptr [edx+1Ch]

call GetDC,HWnd

mov HDC,eax

call GetDeviceCaps, HDC,88 ; LOGPIXELSX

call MulDiv, HMWidth,eax,254*8 ;9ECh ; HIMETRIC_INCH

mov Width, eax

call GetDeviceCaps, HDC,90 ; LOGPIXELSY

call MulDiv, HMHeight,eax,254*8 ;9ECh ; HIMETRIC_INCH

mov Height, eax

call GetClientRect, HWnd, offset RECT

mov edx, GPICTURE ; використання методу OLE

mov edx, [edx] ; IPicture::Render (20h)

mov eax, HMHeight

neg eax

call [edx+20h],GPICTURE,HDC,0,0,Width,Height,0,HMHeight,HMWidth,eax,offset RECT

call ReleaseDC,HWnd,HDC

STOP: call ExitProcess,0

end Start

Зміст звіту

1. Мета роботи.

2. Короткі теоретичні відомості і розрахункові формули.

3. Вихідні дані для виконання роботи (відповідно до отриманого

варіанта).

4. Результати виконання роботи, отримані при різних вихідних даних.

5. Контрольні запитання

6. Висновки.

КОНТРОЛЬНІ ЗАПИТАННЯ

1. Що робить інтерфейс IPicture ?

2. Що робить інтерфейс IStream ?

3. Як отримати код інтерфейсу, якщо знати його назву ?

4. Як отримати адресу процедури виконання того чи іншого метода інтерфейсу ?

5. Що робить метод get_Width інтерфейсу IPicture ?

6. Що робить метод get_Height інтерфейсу IPicture ?

Page 193: для самостійної роботи з дисципліниkkrnu.com.ua/InformZabezpech/met_sam_sist_2017.pdf · для самостійної роботи з дисципліни

7. Що робить метод Render інтерфейсу IPicture ?

8. Як отримати адресу функції API з певної DLL-бібліотеки ?

Рекомендована література 1. Шпак З.Я. Програмування мовою С. Львів: Оріяна-Нова, 2006. – 432 с.: іл.

2. Э.А.Ишкова. С++ Начала программирования. – Москва, Издательство

Бином, 2004г. 3. П.Франка. С++ учебный курс программирования.- Питер, 2002г 4. К. Джамса. Учимся программировать на языке С++. - Издательство «Мир»,

Москва, 2002г 5. С/С++. Структурное программирование. Практикум. Т.А. Павловская,

Ю.А.Щупак. - СПб:Питер, 2003. 6. Т.А. Павловская. Программирование на языке высокого уровня. -

СПб:Питер, 2004.

7. Джек Либерти, Освой самостоятельно С++ за 21 день. - Издательский дом

«Вильямс», Киев, 2004

8. Мюррэй Хилл, Нью Джерси, Бьерн Страуструп, Язык C++

9. Одинцов И.О. Профессиональное программирование. Системный подход.

– СПб.: БХВ – Петербург, 2002. – 512 с.: ил.

10. Стефан Дьюхарст, Кэти Старк Программирование на С++. Пер. с англ. –

Киев: «Диасофт», 1993. – 272 с., ил.

11. Милов А.В. Основы программирования в задачах и примерах: Учебный

курс. – Харьков: Фолио, 2002. – 397 с.

12. Гордеев А.В. Молчанов А. Ю. Системное программное

обеспечение.СПб.;Питер.2002 -736 с.

13. Фельдман С.К. Системное программирование на персональном

компьютере.-М.;ЗАО «Новый издательский дом», 2004.- 512 с.