You are on page 1of 194

НАЦІОНАЛЬНИЙ ТЕХНІЧНИЙ УНІВЕРСИТЕТ УКРАЇНИ

«КИЇВСЬКИЙ ПОЛІТЕХНІЧНИЙ ІНСТИТУТ ім. Ігоря Сікорського»


Кафедра АВТОМАТИЗАЦІЇ ТЕПЛОЕНЕРГЕТИЧНИХ ПРОЦЕСІВ

КОНСПЕКТ ЛЕКЦІЙ

З КУРСУ «ПРОГРАМУВАННЯ - 1. Процедурне програмування»

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

151 – Автоматизація та комп`ютерно-інтегровані технології

Затверджено на засіданні кафедри


автоматизації теплоенергетичних
процесів

протокол № ____ від


____________202___ р.

Київ 2020
-2-

Конспект лекцій кредитного модуля «Програмування – 1.


Процедурне програмування» для бакалаврів напрямку підготовки
«Автоматизація и комп’ютерно-інтегровані технології» // Укладач:
Грудзинський Ю.Є. – Київ: НТУУ "КПІ ім. Сікорського". – 2020 р. – 194
стр.

Укладач Грудзинський Юліан Євгенійович

Відповідальний

редактор ______________________________

Курс присвячений елементарним азам процедурного класичного


програмування на найстарішій, але, як і раніше, молодій мові
програмування високого рівня Сі у стандарті C89.

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


як С++, С#, Java та багатьох інших, широко використовуваних сьогодні
в інформаційних технологіях.

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


мов, студент в майбутньому з легкістю освоює такі затребувані ринку
та промисловості об`єктно-орієнтовані технології, як C++, C#, Java в
силу того, що всі ці мови використовують схожий синтаксис та належать
до єдиної мовної групи, родичем та основою якої є мова Сі. Мова Сі –
це "англійська мова" програмування! Welcome to C!

Київ НТУУ «КПІ ім. І. Сікорського» – 2020 рік


-3-

ЗМІСТ
Тема 1. ОПЕРАЦІЙНА СИСТЕМА І АЛГОРИТМІЧНІ МОВИ. ЗАГАЛЬНЕ ПОНЯТТЯ ПРО
МОВУ СІ 9
1.1. Поняття алгоритмічної мови програмування................................................................................. 9
1.2. Поняття операційної системи. .......................................................................................................10
1.3. Історія мови програмування C. .....................................................................................................11
1.4. Особливості мови С. .......................................................................................................................12
1.5. Структура програми на мові Сі. .....................................................................................................13
1.6. Поняття бібліотеки та компоновки ...............................................................................................13
Тема 2. БАЗОВІ ТИПИ ДАНИХ, ІДЕНТИФІКАТОРИ, ЗМІННІ. ЛОКАЛЬНІ І ГЛОБАЛЬНІ
ЗМІННІ. МОДИФІКАТОРИ.....................................................................................................................15
2.1 Основні типи даних в мові Сі і їхні модифікації ............................................................................15
2.2. Ідентификатори та змінні в мові програмування Сі. ...................................................................16
Локальні змінні ..................................................................................................................................17
Глобальні змінні.................................................................................................................................19
2.3. Кваліфікатор типу ...........................................................................................................................20
Кваліфікатор volatile ..........................................................................................................................21
2.4. Ініціалізація змінних .......................................................................................................................22
Тема 3. КЛАСИ ПАМ'ЯТІ. КОНСТАНТИ. ОПЕРАЦІЇ ТА ВИРАЗИ ..........................................................23
3.1. Специфікатори класу пам’яті .........................................................................................................23
Специфікатор extern ..........................................................................................................................23
Специфікатор static............................................................................................................................25
Специфікатор register ........................................................................................................................26
3.2. Константи ........................................................................................................................................26
Рядкові константи ..............................................................................................................................27
Спеціальні символьні константи ......................................................................................................27
3.3. Операції в мові Сі ............................................................................................................................28
Оператор присвоєння .......................................................................................................................28
Перетворення типів при присвоєннях .............................................................................................29
Множинні присвоєння ......................................................................................................................30
Складене присвоєння........................................................................................................................30
Арифметичні операції .......................................................................................................................31
Операції порівняння і логічні операції .............................................................................................32
Порозрядні операції ..........................................................................................................................33
Операція ? ..........................................................................................................................................34
Операція визначення розміру sizeof ................................................................................................35
-4-
Оператор послідовного обчислення: оператор "кома" .................................................................35
Оператор [] та () .................................................................................................................................35
Звіт по пріорітетам операцій ............................................................................................................36
3.4. Вирази в мові Сі ..............................................................................................................................36
Порядок обчислень ...........................................................................................................................36
Дійсне перетворення типів: операція приведення типів ...............................................................38
Пробіли, круглі дужки та коментарі .................................................................................................38
Тема 4. ОПЕРАТОРИ В МОВІ ПРОГРАМУВАННЯ СІ ...............................................................39
4.1. Умовні оператори ...........................................................................................................................39
Операція "?" ( альтернативна умовному оператору) .....................................................................43
Умовний вираз ...................................................................................................................................44
4.2. Оператор вибору - switch ...............................................................................................................44
Вкладені оператори switch ...............................................................................................................46
4.3. Блок операторів ..............................................................................................................................47
4.4 Оператор циклу for ..........................................................................................................................47
Варіанти циклу for .............................................................................................................................49
Нескінченний цикл ............................................................................................................................52
Цикл for без тіла циклу ......................................................................................................................52
4.5. Цикл while........................................................................................................................................53
4.6. Цикл do-while ..................................................................................................................................55
4.7. Оператори переходу ......................................................................................................................56
Оператор return .................................................................................................................................56
Оператор goto ....................................................................................................................................57
Оператор break ..................................................................................................................................57
Функція exit() ......................................................................................................................................59
Оператор continue .............................................................................................................................59
Тема 5. РОБОТА З МАСИВАМИ ТА РЯДКАМИ ..........................................................................60
5.1. Одновимірні масиви ......................................................................................................................60
5.2. Рядки................................................................................................................................................61
5.3. Двовимірні масиви .........................................................................................................................63
Масиви рядків ....................................................................................................................................65
5.4. Багатовимірні масиви.....................................................................................................................65
5.5. Ініціалізація масивів .......................................................................................................................65
Ініціалізація безрозмірних масивів ..................................................................................................68
Тема 6. ВИКОРИСТАННЯ ПОКАЖЧИКІВ ....................................................................................69
-5-
6.1. Оголошення покажчиків та операції для роботи з ними ............................................................70
6.2. Використання покажчиків у виразах.............................................................................................70
Присвоювання покажчиків ...............................................................................................................70
Перетворення типу покажчика.........................................................................................................71
Адресна арифметика .........................................................................................................................73
Порівняння покажчиків .....................................................................................................................74
6.3. Масиви та покажчики.....................................................................................................................75
6.4. Масиви покажчиків ........................................................................................................................77
6.5. Багаторівнева адресація ................................................................................................................77
6.6. Ініціалізація покажчиків .................................................................................................................79
6.7. Функції динамічного розподілу пам'яті ........................................................................................81
6.8. Динамічне виділення пам’ятi для масивiв ...................................................................................84
6.9. Помилки при роботі з покажчиками ............................................................................................86
Тема 7. ФУНКЦІЇ В МОВІ СІ .............................................................................................................90
7.1. Поняття функції та область її дії .....................................................................................................90
7.2. Аргументи функції ..........................................................................................................................91
Виклик за значенням .........................................................................................................................92
Виклик за посиланням ......................................................................................................................92
7.3. Передача масивів у функцію .........................................................................................................94
7.4. Аргументи функції main(): argv та argc .........................................................................................97
7.5. Оператор return ............................................................................................................................100
Повернення значення функцією ....................................................................................................100
Покажчики, що повертаються ........................................................................................................101
Функція типу void .............................................................................................................................102
7.6. Прототип функції ..........................................................................................................................102
7.7. Аргументи за замовчуванням (С++) ............................................................................................104
7.8. Перевантаження функцій (C++) ...................................................................................................105
7.9. Оголошення списків параметрів змінної довжини ...................................................................108
7.10. Вказівники на функції .................................................................................................................108
7.11. Рекурсивні функції. .....................................................................................................................112
7.11. Тип посилання (С++) ...................................................................................................................115
Приклад використання посилань ...................................................................................................118
Тема 8. КОРИСТУВАЦЬКІ ТИПИ ДАНИХ ...................................................................................119
8.1. Структури .......................................................................................................................................119
Доступ до членів структури.............................................................................................................121
-6-
Присвоювання структур ..................................................................................................................121
8.2. Масиви структур ...........................................................................................................................122
8.3. Передача структур до функції......................................................................................................126
Передача членів структур до функції .............................................................................................126
Передача цілої структури функції...................................................................................................126
8.4. Покажчики на структури ..............................................................................................................128
8.5. Масиви і вкладені структури всередині структур ......................................................................129
8.6. Об’єднання ....................................................................................................................................130
8.7. Бітові поля .....................................................................................................................................133
8.8. Нумератор (Лічений тип) .............................................................................................................137
8.9. Використання операції sizeof для забеспечення кросплатформеності ...................................140
8.10. Визначення нових типів з використанням ключового слова typedef .....................................141
Тема 9. ВВЕДЕННЯ / ВИВЕДЕННЯ У МОВІ СІ.........................................................................144
9.1. Зчитування і запис окремих символів ........................................................................................144
Труднощі використання getchar() і альтернативи ........................................................................145
9.2. Зчитування та запис рядків символів..........................................................................................146
9.3. Функція форматного виведення на консоль printf() ..................................................................149
Виведення символів ........................................................................................................................150
Виведення чисел..............................................................................................................................150
Відображення значення покажчика (адреси) ...............................................................................152
Специфікатор перетворення %n .....................................................................................................152
Модифікатори формату ..................................................................................................................153
Модифікатори мінімальної ширини поля .....................................................................................153
Модифікатори точності ...................................................................................................................153
Вирівнювання значення в полі виведення ....................................................................................154
Обробка даних інших типів.............................................................................................................155
Модифікатор * и # ...........................................................................................................................155
9.4. Функція форматного введення з консолі scanf()........................................................................156
Специфікатори перетворення ........................................................................................................156
Введення чисел ................................................................................................................................157
Введення цілих значень без знака .................................................................................................158
Зчитування поодиноких символів з допомогою scanf() ...............................................................158
Зчитування рядків ............................................................................................................................158
Введення адреси .............................................................................................................................159
Специфікатор %n..............................................................................................................................159
-7-
Використання набору сканованих символів .................................................................................160
Пропуск зайвих розділювачів у вхідному рядку ...........................................................................161
Символи у керуючому рядку, що не є розділювачами ................................................................161
Аргументи функції scanf()................................................................................................................162
Модифікатори формату ..................................................................................................................162
Придушення введення ....................................................................................................................163
9.5. Потоки і файли у мові Сі ...............................................................................................................163
Потоки...............................................................................................................................................163
Текстові потоки ................................................................................................................................163
Двійкові потоки ................................................................................................................................164
Файли ................................................................................................................................................164
9.6. Основні функції файлової системи Сі ..........................................................................................165
9.7. Відкриття і закриття файлу ...........................................................................................................167
9.8. Запис та зчитування поодинокого символу (байту) ..................................................................170
9.9. Використання функцій fopen(), getc(), putc(), та fclose() ............................................................171
9.10. Використання feof() ....................................................................................................................173
9.11. Введення / виведення рядків: функції fgets() та fputs() ..........................................................173
9.12. Функція rewind() ..........................................................................................................................173
9.13. Функції ferror() и perror() ............................................................................................................174
9.14. Стирання файлів і дозапис потоків ...........................................................................................176
9.15. Функції fread() и fwrite() .............................................................................................................177
9.16. Введення / виведеня при прямому доступі: функція fseek() ..................................................179
9.17. Різновиди функцій prinf() і scanf() .............................................................................................180
9.18. Стандартні потоки і перенаправлення введення/виведення ................................................181
Тема 10. ПРЕПРОЦЕСОР ................................................................................................................184
10.1. Директиви #define та #undef ......................................................................................................184
Визначення макросів з формальними параметрами ...................................................................186
Директива #undef ............................................................................................................................186
10.2. Директиви #error і #include ........................................................................................................187
10.3. Директиви умовної компіляції ..................................................................................................188
Директиви #if, #else, #elif і #endif....................................................................................................188
Директиви #ifdef і #ifndef ................................................................................................................190
Використання defined ......................................................................................................................191
10.4. Директиви #line, #pragma і оператори препроцесора # і ## ..................................................191
10.5. Імена передумовлених макрокоманд ......................................................................................193
-8-
СПИСОК ЛІТЕРАТУРИ ...............................................................................................................................194
-9-

ТЕМА 1. ОПЕРАЦІЙНА СИСТЕМА І АЛГОРИТМІЧНІ МОВИ.


ЗАГАЛЬНЕ ПОНЯТТЯ ПРО МОВУ СІ

1.1. Поняття алгоритмічної мови програмування.


Комп’ютер може виконувати програму тільки в тому випадку, якщо команди, що
в ній містяться, представлені в двійковому машинному коді, тобто виражені на мові,
алфавіт якої складається із логічних одиниць і нулів. Для перших комп’ютерів
програми складались безпосередньо в машинних кодах, що потребувало високої
кваліфікації програмістів і великих затрат праці, тому вже в 40-х роках XX століття
почалась розробка мов програмування, які по своїй лексиці були би максимально
наближеними до природньої мови людини. Такі мови програмування називаються
алгоритмічними.
Проміжним кроком до розробки алгоритмічних мов стала мова Ассемблер. В
Ассемблері команди представляються не двійковими числами, а у вигляді
поєднання символів (мнемонічними кодами), за якими можна відтворити значення
команди, що значно усуває труднощі та недоліки програмування на машинній мові.
Однак Ассемблеру притаманні і власні недоліки - це машинно-орієнтована мова, і
для кожного комп’ютера створюється своя мова Ассемблера. Програмування на
Ассемблері вимагає від програміста значного знання архітектури комп’ютера і
пов'язане зі значними трудовитратами. В той же час, саме завдяки Ассемблеру
можна найкраще використати в програмі ресурси комп’ютера (пам’ять, швидкодія),
тому Ассемблер досі має велику популярність серед професійних програмістів.
Першою алгоритмічною мовою став Fortran, створений в 1957 р. спеціалістами
фірми IBM під керівництвом Джона Бекуса. На сьогоднішній день існує вже досить
багато алгоритмічних мов: Pascal, С, C++, С#, Java та ін.
Алгоритмічні мови та ассемблери відносяться до мов символьного кодування,
тобто до мов, які оперують не машинними кодами, а умовними символьними
позначеннями, тому програми, складені на цих мовах, не можуть бути
безпосередньо виконаними на комп’ютері. Щоб така програма запрацювала, її текст
потрібно перетворити в машинні коди. Для цього існуюють спеціальні програми-
перекладачі (транслятори). Розрізняють два види трансляторів - компілятор та
інтерпретатор. Компілятор транслює програму одразу всю, і тільки після цього
можливе її виконання. Інтерпретатор – це простіший транслятор, який послідовно
транслює і зразу виконує окремі оператори програми.
В найпростішому випадку інтерпретатор читає вихідний текст програми по
одному рядку за раз, виконує цей рядок і тільки після цього переходить до
наступного рядка. Так працювали раніше версії мови Basic. В мовах типу Java весь
вихідний текст програми спочатку конвертується в проміжну форму, а потім
інтерпретується. В такому випадку програма такок інтерпретується в процесі
виконання.
Компілятор читає відразу всю програму і конвертує її в об’єктний код, тобто
транслює вихідний текст програми у форму, найбільш придатну для
безпосереднього виконання комп’ютером. Об’єктний код також називають
двійковим або машинним кодом. Коли програма скомпільована, в її коді вже немає
окремих рядків вихідного коду.
В загальному випадку інтерпретована програма виконується повільніше, ніж
скомпільована. Необхідно пам’ятати , що компілятор перетворює вихідний текст
програмы в об’єктний код, який виконується комп’ютером безпосередньо. Значить,
втрата часу на компіляцію відбувається лише одноразово, а у випадку інтерпретації
– час втрачається кожен раз при черговій компіляції фрагменту програми в процесі
її виконання.
- 10 -

1.2. Поняття операційної системи.


Для того, щоб написана та відтрансльована програма могла бути виконаною на
комп’ютері, в пам’ять машини повинні бути попередньо завантажені специальні
службові програми , які керують ресурсами обчислювальної машини та взаємодією
її різноманітних блоків. Цей комплекс службових програм називається операційною
системою (ОС).
Операційна система здійснює дві основні функції:
- забезпечує взаємодію виконуваної користувачем програми з апаратурою
комп’ютера;
- дозволяє людині керувати комп’ютером з допомогою команд, які
вводяться з клавіатури.
Операційна система позбавляє людину від виконання численних другорядних
операцій, як от:
- перевірка роботоздатності після увімкнення;
- розподіл оперативної пам’яті;
- забезпечує взаємодію програм із зовнішніми пристроями і між собою;
- при зчитуванні даних з диска встановлює магнітну головку дисковода
на потрібну доріжку і т.п.
На сьогодні для ПК розроблено кілька операційних систем, орієнтованих на різні
типи мікропроцесорів. На машинах із інтелівською архітектурою (x86 та x64) широко
використовуються зараз такі операційні системи, як Windows, Linux, Unix, Mac OS
та ін.
Одразу, після увімкнення комп’ютера, починає працювати підсистема BIOS.
BIOS (Basic Input-Output System - базова система ввода-виводу). Це програма,
яку записують в мікросхему ПЗУ на заводі при виготовленні комп’ютера. }ї
призначення- налагоджування конфігурації комп’ютера, перевірка роботоздатності
обладнання ПК при його увімкненні, а також завантаження в оперативну пам’ять і
запуск на виконання наступної програми – завантажувача операційної системи Boot
Record.
Boot Record - завантажувач операційної системи. Це маленька програма (не
більше 512 байт), яка зберігається в першому секторі системного диску.
Призначення завантажувача- зчитування з диска в оперативну пам’ять решту
програм ОС та їх запуск на виконання, після чого управління передається вже,
власне, ОС.
В склад будь-якої ОС обов’язково входять драйвери зовнішніх пристроїв. Це
спеціальні програми, призначені для взаємодії комп’ютера із зовнішніми пристроями
- мишою, приводом оптичних дисків, принтером, модемом, графопобудовувачем і
т.і., або для нестандартного використовування зовнішніх пристроїв.
Вся користувацька інформація в ОС зберігається у вигляді одиниці зберігання–
файла.
Файл – це порція довільної інформації(програма, текст, дані, закодована
картинка), яка записуєтсья на носій під індивідуальним ім’ям.
Ім’я кожного файла зберігається в каталозі диска (в заголовку диска). Повне ім’я
файла складаєтсья із 2-х частин: основного імені та розширення. Розширення
відділяється від основного імені точкою:
Основне ім’я . Розширення
Наприклад, "myfile.txt"
Основне ім’я може складатись не більше ,ніж із 255 символів (для Windows) або
8 символів (для MS-DOS) и довільно вибирається так, щоб один файл можна було
відрізнити від інших та отримати уявлення про його вміст. Розширення може
складатись із не більше,як 3-х символів, воно вказує на тип інформації,що
- 11 -
зберігаєься у файлі. Розширення використовувати не обов’язково. Якщо
розширення використовується, то краще давати йому загальновживану назву:
.ASM - в файлі зберігаєтся текст програми, написанної на Ассемблері;
.BAS - текст програми на Бейсіку;
.PAS - текст програми на Паскалі;
.C - текст програми на мові Сі;
.CPP – текст програми на мові С++;
.TXT, .DOC - текстові файли довільної тематики;
.DAT - файл даних (вихідні дані для програми, або результати розрахунку);
.BAK - резервна копія файлу, стара версія модифікованного файлу (якщо
файл записується на диск, на якому вже зберігаєтсья записаний раніше
однойменний файл, то старіша версія файла не стирається, замість цього в її
імені розширення автоматично змінюється на ".bak");
.BAT - пакетний файл - програма, що складаєтсья із команд ОС;
.EXE, .COM - файли, що містять програми, представлені в машиних кодах
(результат трансляції програм, написаних на алгоритмічних мовах).
В основному імені файлу і в розширені допускається використання латинських
букв, цифр та деяких символів - тире, підкреслення, крапка, дужки і т.п.
В каталог диску окрім імен файлів можуть також входити інші каталоги
(підкаталоги першого рівня), які в свою чергу можуть включати в себе як файли, так
і каталоги (підкаталоги 2-го рівня). Таким чином формується "деревовидна"
структура каталогів, що має на самому верхньому рівні єдиний головний каталог
(корінний каталог), до якого сходяться численні гілки підкаталогів. Каталогам, як і
файлам, даються імена (кореневий каталог лишається безіменним). Вимоги до імен
каталогів висуваються ті ж, що і до імен файлів, але розширення зазвичай не
використовується.
1.3. Історія мови програмування C.
Мова С була розроблена та реалізована в період з 1969 по 1973 роки в
лабораторіях американської телекомунікаційної компанії Bell Labs Деннісом Рітчі
для комп’ютера DEC PDP-11 в операційній системі Unix.
Протягом багатьох років стандартом С була фактично версія, яку ставили поряд
із операційною системою Unix. Ця версія вперше була описана Брайаном
Керніганом та Деннісом Рітчі в книзі "Мова програмування С" (1978) Влітку 1983
року був створений комітет з освіти за мовою С стандарту ANSI (American National
Standards Institute — Національний інститут стандартизації США). Варто зазначити,
що процес стандартизації забрав досить чималий період— 6 років.
Стандарт ANSI був остаточо прийнятим в січні 1989 року та вперше
опублікований на початку 1990 року. Цей стандарт був також прийнятий
організацією ISO (International Standards Organization — Міжнародна організація зі
стандартизації), тому він називається стандартом ANSI/ISO мови С. В 1995 році
була прийнята 1-ша Поправка до стандарту С, згідно якій , серед іншого, були додані
кілька бібліотечних функцій. В 1989 році стандарт С разом із 1-ою Поправкой став
базовим документом Стандарту C++, визначаючого С ,як підмножину C++. Версію
С, визначену стандартом 1989 року ,зазвичай називають С89.
Протгом 90-х років увага програмістів була привернута головним чином до
розвитку стандарту C++. Тим часом розробка С також продовжувалась , приводячи
в 1999 році до появи стандарту С, який прийнято називати С99. В цілому С99 зберіг
всі основні риси С89, тобто ,можна сказати, що мова С лишилась самою собою.
Комісія стандартизації С99 приділила основну увагу двом напрямкам: доданню
кількох числових бібліотек та розвитку нових вузькоспеціалізованих засобів , таких
як масиви змінної довжини та модифікатор покажчика restrict.
- 12 -
Однак і на цьому розвиток мови не зупинився. 8 грудня 2011 року опубліковано
новий стандарт для мови С (ISO/IEC 9899:2011)[5]. Серед основних змін тут,
наприклад, введення підтримки багатопоточного та асинхронного програмування .
Завдяки цим нововведенням мова С знову опинилася на передньому краї
розвитку мов програмування .
1.4. Особливості мови С.
Мова програмування С вирізняється мінімалізмом. Автори мови хотіли , щоб
програми на ній легко компілювались за допомогою однопрохідного компілятора,
щоб кожній елементарній складовій програми після компіляції відповідало досить
невелике число машинних команд, а використовування базових елементів мови не
зачіпало бібліотеку часу виконання . Однопрохідний компілятор компілює програму,
не повертаючись назад, до вже обробленого тексту.Тому використанню функції та
змінних в С повинно передувати їх оголошення . Код на С можна легко писати на
низькому рівні абстракції, майже як на асемблері. Іноді С називають «універсальним
ассемблером» або «ассемблером високого рівня», що відображає різницю мов
ассемблера для різних платформ та єдинство стандарту С, код якого може бути
скомпільованим без змін практично на будь-якій моделі комп’ютера. Сі часто
називають мовою середнього рівня, враховуючи те, як близько він працює до
реальних пристроїв.
Компілятори Сі розробляються порівняно легко завдяки простоті мови та
малому розміру стандартної бібліотеки. Тому ця мова доступна на
найрізноманітніших платформах (можливо, кількість цих платформ ширша, ніж у
будь-якої іншої мови програмування). Також, не дивлячись на свою низькорівневу
природу, мова дозволяє створювати кросплатформові програми та підтримує у
цьому програміста. Програми, згідно стандарту мови , можуть компілюватись на
найрізноманітніших платформах.
Сі створювався з одною важливою ціллю: зробити більш простим написання
великих програм із мінімумом помилок за правилами процедурного програмування,
не додаючи до вихідного коду програм зайвих витрат для компілятора, як це завжди
роблять мови дуже високого рівня, на зразок Бейсіка. З цього боку Сі пропонує
наступні важливі особливості:
 просту мовну базу, з якої винесені в бібліотеки багато вагомих
можливостей, на зразок математичних функцій чи функцій управління
файлами;
 орієнтацію на процедурне програмування , яка забезпечує зручність
застосування структурного стилю програмування;
 систему типів, яка запобігає безглуздим операціям;
 використання препроцесора для визначення макросів та включення в
основний текст програми додаткового коду з зовнішніх файлів із вихідним
кодом;
 безпосередній доступ до пам’яті комп’ютера через використання
покажчиків;
 мінімальне число ключових слів;
 передачу параметрів у функцію за значенням, а не за посиланням (при
цьому передача за посиланням емулюється за допомогою покажчиків);
 покажчики на функції та статичні змінні ;
 області дії імен;
 структури та об’єднання— визначені користувачем агрегатні типи даних,
якими можна маніпулювати, як єдиним цілим.
- 13 -

1.5. Структура програми на мові Сі.


В мові Сі розрізняються верхній та нижній регістри символів: else — ключове
слово, a ELSE - ні. В програмі ключове слово може бути використане тільки як
ключове слово, тобто ніколи не допускаєтсья його використання в якості змінної чи
імені функції.
Будь-яка програма на Сі складається із одної або кількох функцій. Обов’язково
повинна бути визначена єдина головна функція main(), саме з неї завжди
починаєтсья виконання програми. В хорошому вихідному тексті програми головна
функція завжди містить оператори, що відображають сутність вирішуваної задачі,
частіше за все це виклики функцій. Хоча main() і не є ключовим словом, відноситися
до нього слід, як до ключового. Наприклад, не слід використовувати main ,як ім’я
змінної , так, як це може порушити роботу транслятора.
Структура програми Сі зображена на Рис. 0.1, тут f1() - fN() означають функції,
написані програмістом.

<Оголошення глобальних змінних>

<тип_ значення_що_повертається> f1(<список_ параметрів_1>) {


послідовність операторів
}

<тип_ значення_що_повертається> f2(<список_ параметрів_2>) {


послідовність операторів
}
.
.
.
<тип_ значення_що_повертається> fN(<список_параметрів_N>) {
послідовність операторів
}

int main(<список параметрів>) {


послідовність операторів
}

Рис. 0.1. Структура програми на мові Сі

1.6. Поняття бібліотеки та компоновки


Слід зазначити, що на Сі у принципі можливо створити програму, що містить
тільки імена змінних та ключові слова. Але зазвичай так не чинять, тому що в Сі
немає ключових слів для виконання багатьох операцій, наприклад таких ,як
ввід/вивід, обчислення математичних функцій, обробка рядків та ін. Тому в
більшості програм присутні виклики різних функцій, що зберігаються в бібліотеці
стандартних функцій Сі.
Всі компілятори Сі поставляються разом із бібліотекою стандартних функцій,
призначечних для виконання найбільш загальних завдань . Стандарт Сі визначає
мінімальний набір функцій, які повинні підтримуватись кожним компілятором.
Зазвичай бібліотеки , що поставляються з компіляторами, мають ще багато інших,
додаткових функцій. Наприклад, в стандартній бібліотеці немає функцій для роботи
з графікою, проте вони є майже в кожному компіляторі.
- 14 -
При виклику бібліотечної функції компілятор "запам’ятовує" її ім’я. Потім
компонувальник зв’язує код вихідної програми з об’єктним кодом, вже знайденим у
стандартній бібліотеці. Цей процес називається компонуванням (редагуванням
зв’язків). У певних компіляторах є власний компонувальник, інші ж користуються
стандартним , який поставляється разом із операційною системою.
В бібліотеці функції зберігаються в форматі ,можливому для переміщення. Це
означає, що адреси машинних інструкцій в пам’яті являются не абсолютними, а
відносними. При компоновці програми з функціями зі стандартної бібліотеки ці
відносні адреси , або зміщення, використовуються для визначення дійсних адрес.
Бібліотека стандартних функцій містить велику кількість функцій, необхідних
для написання програми. Це свого роду «цеглинки», з яких програміст збирає
програму. Крім цього, він може написати свою функцію та помістити її в біблітеку.
Коротка програма на мові С може складатись всього лише із одного файлу
вихідного тексту. Однак, при збільшенні довжини програми збільшується також і час
компиляції. Тому частіше програма на Сі складається із двох чи більше файлів, що
компілюються окремо. Скомпільовані файли програми компонуються з
процедурами з бібліотеки, формуючи таким чином об’єктний код програми.
Перевага окремої компіляції в тому, що при зміні одного файлу немає необхідності
перекомпільовувати заново всю програму. При роботі зі складними проектами це
економить досить багато часу. Окрема компіляція дозволяє також кільком
програмістам спільно та незалежно працювати над одним проектом, так як вона
являє собою засіб організаціїї вихідного тексту програми для великого проекту.
Скомпільована Сі-програма має завжди чотири окремі незалежні одна від одної
області пам’яті. Перша— це область пам’яті, що містить виконуваний код програми.
У другій області зберігаються глобальні змінні. Дві області, що залишились– це стек
та динамічна пам’ять під змінні, що виділяється в процесі роботи програми (часто
іменована купою heap). Стек використовується для збергігання допоміжних змінних
під час виконання програми. Тут знаходяться адреси повернення функцій,
аргументи функцій, локальні змінні та ін. Поточний стан процесора також
зберігається в стеці. Динамічно пам’ять, або купа — це така область пам’яті, в якій
програма може отримувати місце під додаткові необхідні змінні в процесі свого
виконання. На Рис. 0.1 показано, як розподіляється пам’ять під час виконання
програми. Але не слід забувати, що конкретний розподіл може різнитися залежно
від типу процесора та реалізації мови.
Стек


Динамічна область пам’яті
Глобальні та статичні змінні
Код програми
Рис. 0.1. Розподіл пам’яті (карта пам’яті) при виконанні програми, написаної на мові

- 15 -

ТЕМА 2. БАЗОВІ ТИПИ ДАНИХ, ІДЕНТИФІКАТОРИ, ЗМІННІ.


ЛОКАЛЬНІ І ГЛОБАЛЬНІ ЗМІННІ. МОДИФІКАТОРИ

2.1 Основні типи даних в мові Сі і їхні модифікації


Стандарт С89 визначає шість фундаментальних типів даних: char— символьні
дані, int — цілі, float — с плаваючою точкой, double — подвійної точності, long
double – розширеної точності, void — без значення. На основі цих типів
утворюються інші типи даних. Розмір (об’єм займаної пам’яті) і діапазон значень
типу int з різною разрядністю процесора та компілятором можуть дещо
змінюватися. Однак, об'єкт типу char завжди дорівнює 1 байт. Розмір int, як
правило, співпадає з розміром слова в конкретному середовищі програмування. У
більшості випадків, в 16-розрядному середовищі тип int займає 16 біт, а в 32/64 -
розрядному (Windows XP/Vista/7) – 32 біти. Однак, ви не можете покладатися на це
повністю, особливо при переносі програми на іншу платформу. Майте на увазі, що
стандарт зумовлює лише мінімальний діапазон значень для кожного типу даних, а
не сам поточний розмір у байтах.
Змінні сhar використовуються для зберігання символів стандарту ASCII;
символи, які не включені в цей набір, різними компіляторами обробляються по-
різному.
Діапазон значень типів float та double наведено нижче в таблиці.

Таблиця 2.1. Всі типи даних, визначених стандартом С89

Типовий Мінімально допустимий діапазон


Тип
розмір в бітах значень

char 8 від 0 до 255

unsigned char 8 від 0 до 255

signed char 8 від -128 до 127

від -32768 до 32767 (або від -2 147 483 648 до 2 147


int 16 або 32
483 647)

unsigned int 16 або 32 від 0 до 65535 (або від 0 до 4 294 967 295)

signed int 16 або 32 те ж, що й int

short int 16 Від -32768 до 32767

unsigned short int 16 від 0 до 65535

signed short int 16 те ж, що й short int

long int 32 від -2 147 483 648 до 2 147 483 647

signed long int 32 те ж, что й long int


- 16 -

unsigned long int 32 від 0 до 4 294 967 295

float (одинарна від abs(+/-1,2E-38) до abs(+/-3,4E+38), с точністю


32
точність) не менше 6 значущих десяткових цифр

double (подвійна від abs(+/-2,3E-308) до abs(+/-1,7E+308), с точністю


64
не менше 16 значущих десяткових цифр
точність)

від abs(+/-3,4E-4932) до abs(+/-1,1E+4932), с


long double 80
точністю не менше 19 значущих десяткових цифр
(розширена точність)

В таблиці приведені мінімальні можливі, а не типові діапазони значень.


Тип void служить для оголошення функції, яка не повертає значень, або для
створення універсального покажчика.
Базові типи даних (окрім void) можуть мати різні специфікатори, які передують
їм в тексті программ. Специфікатор типу так змінює значення базового типу, щоб він
більш точно відповідав своєму призначенню у програмі. Повний список
специфікаторів типів:

signed
unsigned
long
short

Базовий тип int може бути модифікованим кожним із цих специфікаторів. Тип
char модифікується з допомогою unsigned та signed, double — з допомогою long.
Для цілих можна використовувати специфікатор signed, але в цьому нема
необхідності тому, що при оголошенні цілого цей спеціфікатор встановлюється за
замовчуванням. Специфікатор signed частіше всього використовується для типу
char, який в певних реалізаціях за замовчуванням може бути беззнаковим.
Якщо специфікатор типу записати сам по собі (без наступного за ним базового
типу), передбачається, що він змінює int. Таким чином, наступні специфікатори
типів є еквівалентними:

Таблиця 2.2. Таблиця еквівалентності специфікаторів типу

Специфікатор Те ж саме
Signed signed int
Unsigned unsigned int
Long long int
Short short int

Хоча базовий тип int і пропонується за замовчуванням, його, тим не менш,


зазвичай вказують явно.
2.2. Ідентификатори та змінні в мові програмування Сі.
В мові Сі імена змінних, функцій, тегів, тощо. називаються ідентифікаторами.
Довжина ідентифікатора (кількість символів, що включає ідентифікатор) є
натуральним числом, зазвичай ідентифікатор являє собою послідовність з одного
- 17 -
або кількох символів. Першим символом має бути латинська літера або символ
підкреслювання, а подальші символи повинні бути латинськмими літерами,
цифрами або символами підкреслення. Нижче наведено приклади правильних і
неправильних записів ідентифікаторів.

Таблиця 2.3. Правильні і неправильні імена ідентифікаторів

Правильні Неправильні
count 1count
test23 hi!here
high_balance high...balance

На мові Сі ідентифікатор може бути будь-якої довжини, але не всі його символи
є значущими. Пояснимо це на прикладі внутрішніх та зовнішніх ідентифікаторів.
Зовнішні ідентифікатори беруть участь у зовнішніх процесах редагування зв’язків
між частинами одної програми. Ці ідентифікатори, що називаються зовнішніми
іменами, позначають імена функцій і глобальних змінних, які використовуються
разом в різних вихідних файлах. Якщо ідентифікатор не бере участі в процесі
редагування зовнішніх посилань, то він називається внутрішнім іменем. До таких
імен належать, наприклад, імена локальних змінних. У стандарті С89 значущими є
принаймні перші 6 символів зовнішнього імені і перші 31 символ внутрішнього імені.
Символи на верхньому та нижньому регістрах розглядаються як різні символи.
Тому count, Count і COUNT є трьома різними ідентифікаторами. Ідентифікатор не
може співпадати із ключовим словом Сі або з іменем бібліотечной функції.
Змінна – є іменованою частиною пам'яті, яка зберігає значення, яке може бути
змінено програмою. Усі використовувані в програмі змінні повинні бути
оголошеними до їх першого використання. Загальний вигляд оголошення має бути
приблизно таким:

тип список_змінних;

Тут тип означає один із базових або оголошених програмістом типів (якщо
необхідно — з одним або кількома специфікаторами), а список_змінних складається
з одного або більше ідентифікаторів, розділених комами. Нижче наведені приклади
оголошень:

int i, j, l;
short int si;
unsigned int ui;
double balance, profit, loss;

Необхідно пам'ятати, що в Сі ім'я змінної ніколи не визначає її тип.


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

Локальні змінні

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


змінними. Локальну змінну можна використовувати тільки всередині блока, в якому
вона оголошена. Іншими словами, локальна змінна стає невидимою за межами
- 18 -
свого блоку (блок програми - це описи та інструкції, об'єднані в одну конструкцію
шляхом розміщення їх у фігурних дужках).
Локальні змінні існують тільки під час виконання програмного блоку, в якому
вони оголошені, створюються вони при вході в блок, а руйнуються - при виході з
нього. Більш того, змінна, оголошена в одному блоці, не має ніякого відношення до
змінної з тим же ім'ям, оголошеної в іншому блоці. Частіше всього блоком програми,
в якій оголошені локальні змінні, є функції. Розглянемо, для прикладу, наступні дві
функції:

void func1(void) {
int x;
x = 10;
}

void func2(void) {
int x;
x = -199;
}

Ціла змінна х оголошена двічі: один раз в func1() і другий - в func2(). При цьому
змінна х в одній функції ніяк не пов'язана і ніяк не впливає на змінну з тим же ім'ям
в іншій функції. Це відбувається тому, що локальна змінна видима тільки всередині
блоку, в якому вона оголошена, за межами цього блоку вона невидима.
З міркувань зручності і в силу усталеної традиції, всі локальні змінні функції
найчастіше оголошуються на самому початку функції, відразу після відкриття
фігурної дужки. Однак можна оголосити локальну змінну і всередині блоку програми
(блок функції - це окремий випадок блоку програми). Наприклад:

void main(void) {
int t;
char c;

scanf("%d%*c", &t);
if (t == 1) {
char s[80]; /* Ця змінна створюється тільки
при вході в цей блок */
printf("Name:");
gets_s(s);
/* певні оператори внутрішнього блоку ... */
}

/* тут змінна s вже невидима */


}
У цьому прикладі локальна змінна s створюється при вході в блок if і руйнується
при виході з нього. Отже, змінна s видима тільки всередині блоку if і не може бути
використана ні в яких інших місцях, навіть якщо вони знаходяться всередині функції,
що містить цей блок.
Оголошення змінних всередині блоку програми допомагає уникнути небажаних
побічних ефектів. Змінна не існує поза блоком, в якому вона оголошена, отже,
"стороння" ділянка програми не зможе випадково змінити її значення.
- 19 -
Якщо імена змінних, оголошених у внутрішньому і зовнішньому (по відношенню
до нього) блоках збігаються, то змінна внутрішнього блоку "ховає" (тобто приховує,
робить невидимою) змінну зовнішнього блоку. Розглянемо наступний приклад:

#include <stdio.h>

int main(void) {
int x;

x = 10;

if (x == 10) {
int x; /* ця x ховає зовнішній x */
x = 99;
printf("Внутрішня x: %d\n", x);
}

printf("Зовнішня x: %d\n", x);


return 0;
}

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


Внутрішня х: 99
Зовнішня х: 10

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


зовнішню змінну х. Отже, внутрішня і зовнішня х - це два різних об'єкта. Коли
внутрішній блок закінчується, зовнішня х знову стає видимою.
Так як локальні змінні створюються і знищуються при кожному вході і виході з
блоку, їх значення втрачається кожен раз, коли програма виходить з блоку. Це
необхідно враховувати при виконанні функції. Локальна змінна створюється при
вході в функцію і руйнується при виході з неї. Це означає, що локальна змінна не
зберігає своє значення в період між викликами (проте можна дати вказівку
компілятору зберегти значення локальної змінної, для цього потрібно оголосити її з
модифікатором static).
За замовчуванням локальні змінні зберігаються в стекові. Стек - динамічно
змінювана область пам'яті. Ось чому в загальному випадку локальні змінні не
зберігають своє значення в період між викликами функцій.
Локальні змінні можна ініціалізувати будь-яким наперед заданим значенням. Це
значення буде присвоюватися змінній кожен раз при вході в той блок програми, в
якому вона оголошена.

Глобальні змінні

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


будь-якому місці програми. Вони зберігають своє значення протягом всієї роботи
програми. Щоб створити глобальну змінну, її необхідно оголосити за межами
функції. Глобальна змінна може бути використана в будь-якому виразі, незалежно
від того, в якому блоці цей вираз використовується. Оголошення глобальної змінної
може перебувати в будь-якому місці перед першим використанням цієї змінної, але
тільки не всередині функції. Рекомендується і зазвичай прийнято оголошувати
глобальні змінні в верхній частині програми, до функції main().
- 20 -
Виконується таке правило: якщо локальна і глобальна змінні мають одне і те ж
ім'я, то при зверненні за цим ім'ям всередині блоку, в якому оголошена локальна
змінна, відбувається посилання на локальну змінну, а на глобальну змінну це ніяк
не впливає.
Глобальні змінні зберігаються в окремій фіксованій області пам'яті, створеній
компілятором спеціально для цього. Глобальні змінні використовуються в тих
випадках, коли різні модулі програми використовують одні і ті ж дані. Однак
рекомендується уникати зайвого використання глобальних змінних, тому що вони
займають пам'ять протягом усього часу виконання програми, а не тільки тоді, коли
вони необхідні. Крім того, і це ще важливіше, використання глобальної змінної
робить функцію менш універсальною, тому що в цьому випадку функція
використовує щось визначене поза нею. До того ж велика кількість глобальних
змінних легко призводить до помилок в програмі через небажані побічні ефекти. При
збільшенні розміру програми серйозною проблемою стає довільна зміна значення
глобальної змінної іншою частиною програми, а коли глобальних змінних багато,
запобігти цьому дуже важко.
2.3. Кваліфікатор типу
У мові Сі визначаються кваліфікатори типу, що вказують на доступність і
модифікованість змінної. Стандарт С89 визначає два кваліфікатора: const і volatile.
Кваліфікатор типу повинен передувати імені типу, який він кваліфікує (уточнює).
Кваліфікатор const
Змінна, до якої в оголошенні (декларації) застосований кваліфікатор const, не
може змінювати своє значення у процесі роботи програми. Її можна тільки
ініціалізувати, тобто присвоїти їй значення на початку виконання програми.
Компілятор може помістити змінну цього типу в постійний запам'ятовуючий
пристрій, так зване ПЗУ. Наприклад, в оголошенні
const int a = 10;
створюється змінна з ім'ям а, причому їй присвоюється початкове значення 10, яке
в подальшому в програмі змінити вже ніяк не можна. Змінну, до якої в оголошенні
застосований кваліфікатор const, можна використовувати в різних виразах. Однак
своє значення вона може отримати тільки в результаті ініціалізації або за
допомогою апаратно-залежних засобів.
Кваліфікатор const часто використовується для того, щоб запобігти зміні
функцією об'єкта, на який вказує аргумент функції. Без нього при передачі у функцію
покажчика ця функція може змінити об'єкт, на який він вказує. Однак якщо в
оголошенні параметра-покажчика застосований кваліфікатор const, функція не
зможе змінити цей об'єкт. У наступному прикладі функція sp_to_dash() друкує мінус
замість кожного пробільного символа в рядку, переданому їй як аргумент. Тобто
рядок "тестовий приклад" буде надруковано як "тестовий-приклад". Застосування
кваліфікатора const в оголошенні параметра функції гарантує, що всередині
функцій об'єкт, на який вказує параметр функції, не буде змінений.

#include <stdio.h>

void sp_to_dash(const char *str);

int main(void) {
sp_to_dash("тестовий приклад");
return 0;
- 21 -
}

void sp_to_dash(const char *str) {


while (*str) {
if (*str == ' ')
printf("%c", '-');
else
printf("%c", *str);
str++;
}
}

Якщо написати sp_to_dash() таким чином, що всередині функції рядок


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

/ * Неправильний приклад. * /
void sp_to_dash(const char *str) {
while(*str) {
if (*str==' ' ) *str = '-'; /* це неправильно */
printf("%c", *str);
str++;
}
}

Кваліфікатор const використовується в оголошеннях параметрів багатьох


функцій стандартної бібліотеки. Наприклад, прототип функції strlen() виглядає так:

size_t strlen (const char * str);

Застосування кваліфікатора const в оголошенні str гарантує, що функція не змінить


рядок, на який вказує str. Якщо функція стандартної бібліотеки не призначена для
зміни аргументу, то практично завжди в оголошенні покажчика на аргумент
застосовується кваліфікатор const.
Програміст теж може застосовувати кваліфікатор const для того, щоб
гарантувати збереження об'єкта. Але слід пам'ятати, що змінна, навіть якщо до неї
застосований кваліфікатор const, може бути змінена в результаті якого-небудь
зовнішнього по відношенню до програми впливу. Наприклад, їй може бути
присвоєно значення якимось пристроєм. Однак застосування кваліфікатора const в
оголошенні змінної гарантує, що її зміна може відбутися тільки в ході зовнішньої, по
відношенню до програми, події.

Кваліфікатор volatile

Кваліфікатор volatile вказує компілятору на те, що значення змінної може


змінитися незалежно від програми, тобто внаслідок впливу ще чого-небудь, що не
є оператором програми. Наприклад, адресу глобальної змінної можна передати в
функцію операційної системи, яка стежить за часом, і тоді ця змінна буде містити
системний час. У цьому випадку значення змінної буде змінюватися без участі будь-
якого оператора програми. Знання таких подробиць важливо тому, що більшість
компіляторів Сі автоматично оптимізують деякі вирази, припускаючи при цьому
незмінність змінної, якщо вона не зустрічається в лівій частині оператора
- 22 -
присвоювання. В цьому випадку при черговому посиланню на змінну може
використовуватися її попереднє значення. Деякі компілятори змінюють порядок
обчислень в виразах, що може привести до помилки, якщо в виразі присутня змінна,
яка змінюється поза програмою. Кваліфікатор volatile запобігає такій «оптимізації»
програми.
Кваліфікатори const і volatile можуть застосовуватися і спільно. Наприклад, якщо
0x30 - адреса порту, значення в якому може задаватися тільки ззовні, то таке
оголошення попередить можливість утворення небажаних побічних ефектів:

const volatile char *port = 0x30;

2.4. Ініціалізація змінних


При оголошенні змінної вона може бути проініціалізована. Для цього потрібно
після її оголошення поставити знак рівності і константу, тобто загальна форма
ініціалізації має наступний вигляд:

тип ім'я_змінної = константа;

Приклади ініціалізації змінних:

char ch = 'a';
int first = 0;
double balance = 123.23;

Глобальні і статичні локальні змінні ініціалізуються тільки один раз на початку


роботи програми. А локальні змінні (виключаючи статичні локальні) ініціалізуються
кожен раз при вході в блок, в якому вони оголошені. Неініціалізовані локальні змінні
до першого присвоєння мають довільне значення. Неініціалізовані глобальні і
статичні локальні змінні на початку роботи програми автоматично обнуляються.
- 23 -

ТЕМА 3. КЛАСИ ПАМ'ЯТІ. КОНСТАНТИ. ОПЕРАЦІЇ ТА


ВИРАЗИ

3.1. Специфікатори класу пам’яті


Стандарт Сі підтримує три основних специфікатора класу пам'яті:
extern

static

register

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


змінні в пам'яті. Загальна форма оголошення змінних при цьому така:

специфікатор_класу_пам’яті тип ім'я_змінної;

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

Специфікатор extern

У мові Сі при редагуванні зв'язків до змінної може застосовуватися одне з трьох


зв'язувань: внутрішнє, зовнішнє або ж не відноситься ні до одного з цих типів (в
останньому випадку редагування зв'язків до змінної не застосовується). У
загальному випадку до імен функцій і глобальних змінних застосовується зовнішнє
зв'язування. Це означає, що після редагування зв’язків вони будуть доступні у всіх
файлах, що складають програму. Зовнішні змінні, оголошені зі специфікатором
static, стають видимими тільки на рівні файлу, в якому вони оголошені (тобто, до них
застосовується внутрішнє зв'язування на рівні файлу (не блоку!)). До локальних
змінних зв'язування не застосовується і тому вони доступні тільки всередині своїх
блоків.
Специфікатор extern вказує на те, що до об'єкта застосовується зовнішнє
зв'язування, саме тому вони будуть доступні у всій програмі. Далі розглянемо
поняття оголошення і опису. Оголошення (декларація) декларує ім'я і тип об'єкту.
Опис виділяє для об'єкта ділянку пам'яті, де він буде знаходитися. Один і той же
об'єкт може бути оголошений неодноразово в різних місцях, але описаний він може
бути тільки один раз.
У більшості випадків оголошення змінної є в той же час і її описом. Однак, якщо
перед ім'ям змінної стоїть специфікатор extern, то оголошення змінної може і не
бути її описом. Таким чином, якщо потрібно послатися на змінну, визначену в іншій
частині програми, необхідно оголосити її як зовнішню extern.
Приклад використання зовнішньої змінної без специфікатора extern:

#include "stdio.h"
int first = 10, last = 20;
int main(void) {
printf("%d %d \n", first, last);
return 0;
}

Буде надруковано 10 20

Приклад, коли локальна змінна буде "затінювати" собою глобальну змінну:


- 24 -

#include "stdio.h"
int first = 10, last = 20;
int main(void) {
int first = 30, last = 40;
printf("%d %d \n", first, last);
return 0;
}

Буде надруковано 30 40

Приклад використання специфікатора extern. Глобальні змінні first і last


оголошені після функції main()

.#include <stdio.h>
int main(void) {
extern int first, last; /* використовуються глобальні змінні */
printf("%d %d", first, last);
return 0;
}
/* опис глобальних змінних first та last */
int first = 50, last = 60;

Програма надрукує 50 60, тому що глобальні змінні first і last ініціалізовані цими
значеннями. Оголошення extern повідомляє компілятору, що змінні first і last
визначені в іншому місці, тому програма компілюється без помилок, не дивлячись
навіть на те, що first і last використовуються до свого опису.
В останньому прикладі оголошення змінних зі специфікатором extern необхідно
тільки тому, що вони не були оголошені до main(). Якби їх оголошення зустрілося
перед main(), то у внутрішньому оголошенні зі специфікатором extern не було б
необхідності.
При компіляції виконуються наступні правила. Якщо компілятор знаходить
змінну, що не оголошена всередині блоку, він шукає її оголошення в зовнішніх
блоках. Якщо не знаходить його і там, то шукає серед оголошень глобальних
змінних. У попередньому прикладі, якщо б не було оголошення extern, компілятор
не знайшов би first і last серед глобальних змінних, тому що вони оголошені після
main(). Тут специфікатор extern повідомляє компілятору, що ці змінні будуть
оголошені у файлі пізніше.
Як сказано вище, специфікатор extern дозволяє оголосити змінну, не описуючи
її. Але якщо в оголошенні зі специфікатором extern ініціалізувати змінну, то це
оголошення зразу стає також і описом. При цьому програміст обов'язково повинен
враховувати, що об'єкт може мати багато оголошень, але лише один опис.
Специфікатор extern грає велику роль у програмах, що складаються з багатьох
файлів. У мові Сі програма може бути записана в декількох файлах (працюють
окремі програмісти!), які компілюються окремо, а потім компонуються в одне ціле. В
цьому випадку необхідно якось повідомити всі файли (всю групу програмістів) про
глобальні змінні програми. Найкращий спосіб зробити це - визначити (описати) всі
глобальні змінні в одному файлі і оголосити їх зі специфікатором extern в інших
файлах, як показано нижче.
Файл 1 Файл 2
int x, y; extern int x, y;
char ch; extern char ch;
- 25 -

int main(void) void func22(void)


{ {
/* тіло_прогр. */ x = y / 10;
} }

void func1(void) void func23(void)


{ {
x = 123; y = 10;
} }

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


визначені в інших файлах. Таким чином компілятор дізнається імена і типи змінних,
розміщених в іншому місці, і може окремо компілювати другий файл, нічого не
знаючи про перший. При компонуванні цих двох модулів всі посилання на глобальні
змінні будуть вирішені.
На практиці програмісти зазвичай включають оголошення extern в заголовкові
файли з розширенням .h, які просто підключаються до кожного файла вихідного
тексту програми. Це більш легкий шлях, який до того ж призводить до меншої
кількості помилок, ніж повторення цих оголошень вручну в кожному файлі.

Специфікатор static

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


своєї функції, блока або файла. На відміну від глобальних змінних, вони невидимі
за межами своєї функції, блока або файла, але вони зберігають своє значення між
викликами цієї функції. Ця особливість робить їх корисними в загальних і
бібліотечних функціях, які будуть використовуватися іншими програмістами.
Специфікатор static впливає на локальні і глобальні змінні по-різному.
Локальні статичні змінні
Для локальної змінної, описаної з специфікатором static, компілятор виділяє в
постійне користування ділянку пам'яті в глобальній області даних програми, так
само, як і для глобальних змінних. Корінна відмінність статичних локальних змінних
від глобальних полягає в тому, що статичні локальні змінні видно тільки всередині
блоку, в якому вони оголошені. Говорячи коротко, статичні локальні змінні - це
локальні змінні, що зберігають своє значення між викликами функції.
Статичні локальні змінні дуже важливі при створенні функцій, які працюють
окремо, так як багато процедур вимагають збереження деяких значень між
викликами. Якби не було статичних змінних, замість них довелося б
використовувати глобальні, піддаючи їх ризику не навмисної зміни іншими
ділянками програми.
Статичну локальну змінну можна ініціалізувати. Це значення присвоюється їй
тільки один раз - на початку роботи всієї програми, але не при кожному вході в
блок програми де вона визначена, як для звичайної локальної змінної. У наступному
прикладі функція series() генерує послідовність чисел, на початку роботи програми,
що починається з 123:

#include "stdio.h"

extern int series();


- 26 -
int main(void) {
printf("%d \n", series());
printf("%d \n", series());
printf("%d \n", series());
return 0;
}

int series(void) {
static int series_num = 100;
series_num = series_num + 23;
return series_num;
}
Глобальні статичні змінні
Специфікатор static в оголошенні глобальної змінної змушує компілятор
створити глобальну змінну, видиму тільки в тому файлі, в якому вона оголошена.
Статична глобальна змінна, таким чином, піддається внутрішньому зв'язуванню на
рівні файла, як описано раніше в пункті Специфікатор extern. Це означає, що хоч ця
змінна і глобальна, проте процедури в інших файлах не побачать її і не зможуть
випадково змінити її значення. Цим знижується ризик небажаних побічних ефектів.
Висновок: імена локальних статичних змінних видимі тільки всередині блоку, в
якому вони оголошені; імена глобальних статичних змінних видимі тільки
всередині файлу, в якому вони оголошені.

Специфікатор register

Спочатку специфікатор класу пам'яті register застосовувався тільки до змінних


типу int, char і для покажчиків. Однак стандарт С89 розширив використання
специфікатора register, тепер він може застосовуватися до змінних будь-яких типів.
Визначення специфікатора register істотно розширено. Стандарт С89 просто
декларує "доступ до об'єкта так швидко, як тільки можливо". Практично при цьому
символьні і цілі змінні розміщуються в регістрах процесора (при можливості!). Великі
об'єкти (наприклад, масиви) не можуть поміститися в регістри процесора, однак
компілятор отримує вказівку "подбати" про швидкодію операцій з ними. Залежно від
конкретної реалізації компілятора і операційної системи, змінні register
обробляються по-різному. Іноді специфікатор register просто ігнорується, а змінна
обробляється як звичайна.
Специфікатор register можна застосовувати тільки до локальних змінних і
формальних параметрів функцій. В оголошенні глобальних змінних застосування
специфікатор register не допускається.
У мові Сі за допомогою унарного оператора & (отримання адреси змінної) НЕ
можна отримати адресу регістрової змінної, тому що вона може зберігатися на
регістрі процесора, який не має адреси, а тільки ім'я.
3.2. Константи
Константа – це фіксоване значення, яке не може бути змінено програмою.
Константа може стосуватися будь-якого базового типу. Спосіб подання константи
визначається її типом. Константи також називаються літералами.
Символьні константи беруться в одинарні лапки. Наприклад, 'а' і '%' - це
символьні константи.
- 27 -
char c = 'B';

Тут змінній с присвоюється значення символьної константи 'B'.


Цілі константи визначаються як числа без дробової частини. Наприклад, 10 і -
100 - це цілі константи. Константи в плаваючому форматі записуються як числа з
десяткової точкою, наприклад, 11.123. Допускається також експоненціальне
подання чисел (у вигляді мантиси і порядку): 111.23е-1.
За замовчуванням компілятор приписує цілим константантам тип найменшого
розміру, який може розмістити константу, але не меншим типу int. Таким чином,
якщо цілі числа зазвичай є 32-розрядними, і константа 103000, і константа 10 мають
тип int. Це правило має виняток: всім константам плаваючого формату, навіть
найменшим, приписується тип double (якщо, звичайно, вони сюди поміщаються!).
Визначення типів констант за замовчуванням є цілком задовільним при розробці
більшості програм. Однак, використовуючи суфікси, можна явно вказати тип
числової константи. Якщо після числа в плаваючому форматі стоїть суфікс F, то
вважається, що константа має тип float, а якщо L, то long double. Для цілих типів
суфікс U означає unsigned, a L - long. Тип суфікса не залежить від регістра,
наприклад, як F, так і f визначають константи типу float. Наведемо кілька прикладів:
Таблиця 3.1. Приклади визначення констант в Сі

Тип даних Приклади констант


int 1 123 21000 -243
short int 35000s -34S
unsigned int 10000U 987u 40000U
float 123.23F 4.34e-4f
double 123.23 -0.98765432e-10 1.0
long double 1001.2L 1.0l
Іноді зручніше використовувати не десяткову, а восьмеричну або
шістнадцяткову систему. Шістнадцяткова константа починається з 0х, а вісімкова -
з 0, наприклад:
int hex = 0x80; /* 128 в десятковій системі */
int oct = 012; /* 10 в десятковій системі */

Рядкові константи

Мова Сі підтримує ще один тип констант, а саме - рядкові. Рядок - це


послідовність символів, взятих в подвійні лапки. Наприклад, "тест" - це рядок. У
терміні "рядкова константа" слово "рядкова" не означає рядковий тип даних, такого
в С89 немає, тут це всього лише прикметник.
Не слід плутати поняття рядка і символу. Символьна константа береться в
одинарні лапки, наприклад, 'а'. А запис "а" означає рядок, що складається з одного
символу (але на відміну від одного символу займає ДВА байта: другий байт –
символ завершення рядку '\0' = 0) .

Спеціальні символьні константи

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


в одинарні лапки. Але деякі символи, наприклад, символ повернення каретки,
вимагають спеціального зображення. У мові Сі визначені спеціальні символьні
константи, наведені в таблиці нижче. Іноді їх називають ESC-послідовностями,
- 28 -
керуючими послідовностями і символами зі зворотним слешем. Керуючі
послідовності можна використовувати замість ASCII-кодів для забезпечення кращої
крос-платформеності програми.
Таблиця 3.2. Керуючі символи в Сі

Код Призначення
\b Видалення останнього символа
\f Прокрутка до початку нової сторінки паперу чи екрану
\n Перехід каретки (курсора) на початок нового рядка
\r Повернення каретки (курсора) в початок рядка
\t Горизонтальна табуляція
\" Подвійні лапки
\' Одинарні лапки
\\ Зворотний слеш
\v Вертикальна табуляція
\a Сигнал (бі-іп)
\? Знак питання
\N Вісімкова константа (N - вісімкове представлення)
\xN Шістнадцяткова константа (N – шістнадцяткове представлення)

3.3. Операції в мові Сі


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

Оператор присвоєння

Оператор присвоєння може бути присутнім в будь-якому виразі мови Сі. Цим Сі
відрізняється від більшості інших мов програмування (Pascal, BASIC і FORTRAN), в
яких присвоєння можливо тільки в окремому операторі. Загальна форма оператора
присвоєння:

ім’я_змінної = вираз;

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


Адресатом (одержувачем), тобто лівою частиною оператора присвоєння повинен
бути об'єкт, здатний отримати значення, наприклад, змінна. У книжках по Сі і в
повідомленнях компілятора часто зустрічаються терміни lvalue (left side value) і
rvalue (right side value). Попросту кажучи, lvalue - це об'єкт, який може стояти в лівій
частині оператора присвоєння. Він, також, називається модифіковуваним (lvalue).
Термін rvalue означає значення виразу в правій частині оператора присвоєння, яке
не може бути змінено. Об'єкт rvalue може бути присутнім лише в правій частині
оператора присвоєння.
- 29 -

Перетворення типів при присвоєннях

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


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

#include "stdio.h"

int main(void) {
int i = 32767;
int i2 = 1025;
unsigned int u = 10;
char ch = 'A';
double f = 1.5;

ch = i; /* 1-й рядок */
printf("%d \n", ch);

ch = i2; /* 2-й рядок */


printf("%d \n", ch);

ch = u; /* 3-й рядок */
printf("%u \n", ch);

i = f; /* 4-й рядок */
printf("%d \n", i);

f = ch; /* 5-й рядок */


printf("%e \n", f);

f = i; /* 6-й рядок */
printf("%e \n", f);

return 0;
}

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


перетвореннях. Необхідно пам'ятати, що перетворення int у float, або float у double
не підвищує точність обчислень. При такому перетворенні тільки змінюється форма
подання числа. Деякі компілятори при перетворенні char в int вважають змінну char
позитивною незалежно від її значення. Інші компілятори вважають змінну char
негативною, якщо вона менше 127. Тому для забезпечення мобільності програми
необхідно використовувати змінні типу char для зберігання символів, а змінні типу
signed char і unsigned char - для зберігання чисел.

Таблиця 3.3. Приклади втрати інформації при перетворенні типів

Тип адресата Тип виразу Втрата інформації


Якщо значення > 127, то результат
signed char char
від’ємний
- 30 -

Тип адресата Тип виразу Втрата інформації


int (16-
char Старші 8 біт
розрядний)
int (32-
char Старші 24 біт
розрядний)
char long int Старші 24 біт
int (16-
short int Немає
розрядний)
int (32-
short int Старші 16 біт
розрядний)
int (16-
long int Старші 16 біт
розрядний)
int (32-
long int Немає
розрядний)
int float Дробова частина
float double Результат округляється
double long double Результат округляється

Множинні присвоєння

В одному операторі присвоєння можна присвоїти одне і те ж значення багатьом


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

x = y = z = 0;

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


дуже часто.

Складене присвоєння

Складене присвоєння - це різновид оператора присвоєння, в якій запис


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

x = x+10;

можно записати як

x += 10;

Оператор "+=" повідомляє компілятор, що до змінної х треба добавити 10.


Складені оператори присвоєння існують для всіх бінарних операцій (тобто
операцій, що мають два операнда). Будь-який оператор виду
змінна = змінна оператор вираз;
можна записати як
змінна оператор = вираз;
Складене присвоєння значно компактніше, ніж відповідне просте присвоєння.
- 31 -

Арифметичні операції

У таблиці нижче наведено арифметичні операції Сі. Операції +, -, * і / працюють


так само, як і в більшості інших мов програмування. Їх можна застосовувати майже
до всіх стандартних типів даних. Якщо операція / застосовується до цілого або
символьного типу, то залишок від ділення відкидається.
Таблиця 3.4. Список арифметичних операцій

Оператор Операція
- Віднімання, також унарний мінус
+ Додавання
* Множення
/ Ділення
% Залишок від ділення
-- Декремент, або зменшення
++ Інкремент, або збільшення
Оператор ділення по модулю % в Сі працює так само, як і в інших мовах, його
результатом є залишок від цілочисельного ділення. Цей оператор, проте, не можна
застосовувати до типів даних із плаваючою точкою.
Операції збільшення (інкремента) і зменшення (декремента)
У мові Сі є два корисних оператора, які значно спрощують широко поширені
операції. Це інкремент ++ і декремент --. Оператор ++ збільшує значення операнда
на 1, а -- зменшує на 1.
Як інкремент, так і декремент можуть передувати операнду (префіксна форма)
або слідувати за ним (постфіксна форма). Наприклад

x = x+1;

можна записати як у вигляді

++x;

так і у вигляді

x++;

Тут ніякої різниці між іх виконанням та отримуваним результатом не буде. Тобто, у


такому випадку використання двох різних форм оператора інкременту еквівалентне.
Однак префіксна і постфіксна форми відрізняються при використанні їх у
виразах. Якщо оператор інкремента або декремента передує операнду, то ця
операція виконується до використання результату при обчисленні виразу. Якщо ж
оператор слідує за операндом, то при обчисленні виразу використовується
початкове значення операнда і лише після повного обчислення виразу над
операндом виконуються операції інкремента або декремента. Тобто для виразу ця
операція ніби не існує, вона виконується тільки для операнда після обчислення
значення самого виразу, наприклад:
x = 10;
z = 5;
- 32 -
y = ++x+z;
присвоює у та x значення 16. Однак якщо написати
x = 10;
y = x++ + z;
то змінній у буде присвоєно значення 15, а змінній x – значення 11. В обох випадках
після виконання останнього присвоєння х прийме значення 11, різниця тільки в
тому, коли саме це сталося - до або після присвоєння значення змінній у.
Більшість компіляторів Сі генерують для інкремента і декремента дуже швидкий,
ефективний об'єктний код, значно кращий, ніж для відповідних операторів
присвоєння. Тому всюди, де це можливо, рекомендується використовувати
інкремент і декремент.
Пріоритет виконання арифметичних операторів наступний:
Найвищий ++ --
- (унарний мінус)
*/%
Найнижчий + -
Операції з однаковим пріоритетом виконуються зліва направо. Використовуючи
круглі дужки, можна змінити порядок обчислень. У мові Сі круглі дужки
інтерпретуються компілятором так само, як і в будь-якій іншій мові програмування:
вони ніби надають операції (або послідовності операцій) найвищий пріоритет.

Операції порівняння і логічні операції

Операції порівняння - це операції, в яких значення двох змінних порівнюються


одне з одним. Логічні ж операції реалізують засобами мови Сі операції формальної
логіки. Між логічними операціями і операціями порівняння існує тісний зв'язок:
результати операцій порівняння часто є операндами логічних операцій.
В операціях порівняння і логічних операціях в якості операндів і результатів
операцій використовуються значення ІСТИННЕ (true) і ХИБНЕ (false). У мові Сі
значення ІСТИННЕ може бути представлено будь-яким значенням, відмінним від
нуля. Значення ХИБНЕ представляється лише нулем (всі біти нульові).
Результатом операції порівняння або логічної операції є ІСТИННЕ (true, 1) або
ХИБНЕ (false, 0).
В Таблиця 3.6 нижче наведено повний список операцій порівняння і логічних
операцій. Таблиця істинності логічних операцій має такий вигляд:

Таблиця 3.5. Таблиця істинності логічних операцій

p q p && q p || q !p
0 0 0 0 1
0 1 0 1 1
1 1 1 1 0
1 0 0 1 0
Як операції порівняння, так і логічні операції мають нижчий пріоритет в
порівнянні з арифметичними. Тобто, вираз 10>1+12 інтерпретується як 10 > (1+12).
Результат буде ХИБНИМ.
В одному виразі можна використовувати зразу декілька операцій:
10>5 && !(10<9) || 3<4
У цьому випадку результатом буде ІСТИНА.
- 33 -
Таблиця 3.6. Символи операцій порівняння та логічних операцій

Оператори порівняння
Оператор Операція
> Більше ніж
>= Більше або дорівнює
< Менше ніж
<= Менше або дорівнює
== Дорівнює
!= Не дорівнює
Логічні операції
Оператор Операція
&& І
|| АБО
! НЕ, заперечення
Нижче наведено пріоритет логічних операцій:
Найвищий !
> >= < <=
== !=
&&
Найнижчий ||
Як і в арифметичних виразах, для зміни порядку виконання операцій порівняння
і логічних операцій можна використовувати круглі дужки.

Порозрядні операції

У Сі визначено повний набір порозрядних операцій. Це обумовлено тим, що Сі


було задумано, як мову, покликану в багатьох випадках замінити асемблер, який
здатний оперувати окремими бітами даних. Порозрядні операції - це тестування
(перевірка окремих біт) змінної, зсув байтів змінної вправо (вліво) або присвоєння
значень окремим бітам змінної. Ці операції здійснюються над змінними пам'яті, що
містять дані типу char або int. Інші типи даних не можуть брати участь в
порозрядних операціях. У таблиці нижче наведено повний список символів
порозрядних операцій, виконуваних над окремими розрядами (бітами) операндів.

Таблиця 3.7. Список порозрядних операцій

Оператор Операція
& І
| АБО
^ виключне АБО
~ НЕ (заперечення, доповнення до 1)
>> Зсув вправо
<< Зсув вліво
- 34 -
В автоматизації найбільш часто порозрядні операції застосовуються при
програмуванні перевірок стану пристроїв і механізмів, керування пристроями
шляхом включення / вимкнення пристроїв (шляхом зміни стану окремих біт портів
керування), програмуванні драйверів пристроїв, а також процедур, що виконують
операції над файлами.
В автоматизації часто буває необхідно перевірити один або кілька розрядів
інформаційних регістрів керуючих контролерів на предмет знаходження там
одиниць (скажімо, з метою перевірки стану клапана - відкритий / закритий). Для
цього використовується так звана маска, яка містить в розряді, що необхідно
перевірити - одиницю, а в усіх інших - нулі і порозрядна операція &. Наприклад, ми
хочемо перевірити, чи містить 3-й розряд змінної «а» одиницю, або нуль. Для цього,
наприклад, досить виконати наступний оператор:

a&0x8 ? printf ("Містить") : printf ("Не містить");


/ * Тут 0х8 - маска, шістнадцятковий запис двійкового числа 1000, у третьому
розряді якого знаходиться 1 * /

Операція & призведе до того, що всі розряди змінної а, крім третього, будуть
скинуті в нуль. Отже, якщо у третьому розряді а буде стояти 1, то результат виразу
а&0x8 буде ІСТИНОЮ, в іншому випадку - ХИБНИМ.
Для встановлення певних розрядів числа (наприклад, якщо вони відповідають
за ввімкнення будь-яких пристроїв, наприклад двигунів) використовується операція
порозрядного логічного АБО | і знову маска. Розглянемо попередній приклад. Нехай
нам тепер треба встановити 1 в третьому розряді змінної а. Для цього достатньо
виконати наступну операцію з маскою

a=a|0x8;
/ * Інші біти в а залишаться незмінними! * /

Усі двійкові розряди змінної a при цьому залишаться без змін і лише третій розряд
буде встановлено в 1.
Порозрядні оператори зсуву >> і << зсувають всі біти змінної вправо або вліво.
Загальна форма оператора зсуву вправо:
змінна >> кількість_розрядів
Загальна форма зсуву вліво:
змінна << кількість_розрядів
Під час зсуву бітів в один кінець числа, інший кінець заповнюється нулями. Але
якщо число типу signed int від’ємне, то при зсуві вправо лівий кінець заповнюється
одиницями, так що знак числа зберігається. Порозрядні операції зсуву дуже корисні
при декодуванні виходів зовнішніх пристроїв, наприклад таких, як цифро-аналогові
перетворювачі, а також при зчитуванні інформації про статус пристроїв. Побітові
оператори зсуву можуть швидко множити і ділити цілі числа, за один такт, на два.
Порозрядна операція заперечення (доповнення) ~ інвертує стан кожного біта
операнда. Тобто, 0 перетворює в 1, а 1 - в 0.

Операція ?

У мові Сі визначено потужний і зручний оператор, який часто можна


використовувати замість умовного оператора виду if-then-else. Це тернарний
оператор ?, загальний вигляд якого наступний:
Вираз1 ? Вираз2: Вираз3;
- 35 -
Оператор ? працює наступним чином: спочатку обчислюється Вираз1 і якщо він
істинний, то обчислюється Вираз2 і його значення береться у якості значення
ВСЬОГО виразу; якщо значення Вираз1 є хибним, то обчислюється Вираз3 і вже
його значення береться у якості значення ВСЬОГО виразу. Приклад:

x = 10;
y = x>9 ? 100 : 200;

Змінній у тут буде присвоєно значення 100.

Операція визначення розміру sizeof

Унарна операція sizeof, яка виконується під час компіляції (а не виконання!)


програми, дозволяє визначити довжину операнда в байтах. Наприклад, якщо
компілятор для чисел типу int відводить 4 байта, а для чисел типу double - 8, то
наступна програма надрукує 84.

double f;
printf ("%d ", sizeof f);
printf ("%d", sizeof(int));

Для обчислення розміру типу змінної ім'я типу повинне бути взяте в круглі дужки.
Ім'я змінної брати в дужки не обов'язково, але помилки в цьому не буде.
Результат операції sizeof має тип size_t. Цей тип можна використовувати
всюди, де допустимо використання цілого числа без знака. Оператор sizeof
виконується тільки під час компіляції, його результат в програмі розглядається
як константа.

Оператор послідовного обчислення: оператор "кома"

Оператор "кома" пов'язує воєдино кілька виразів. При обчисленні лівої частини
оператора "кома" завжди мається на увазі, що вона має тип void. Це означає, що
вираз, що стоїть праворуч після оператора "кома", є значенням всього розділеного
комами виразу. Наприклад, оператор x = (y = 3, y + 1); спочатку присвоює «у»
значення 3, а потім присвоює «х» значення 4. Дужки тут обов'язкові, тому що
пріоритет оператора "кома" менший, ніж оператора присвоєння.
В операторі "кома" виконується послідовність операцій. Якщо цей оператор
стоїть в правій частині оператора присвоєння, то його результатом завжди є вираз,
що стоїть останнім у списку.

Оператор [] та ()

Круглі дужки є оператором, що підвищує пріоритет виконання операцій, які в них


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

#include <stdio.h>
int main(void) {
char s[80];
s[3] = 'X';
printf("%c \n", s[3]);
- 36 -
return 0;
}

значення символьної константи 'Х' спочатку присвоюється четвертому елементу


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

Звіт по пріорітетам операцій

В Таблиця 3.8 нижче наведені пріоритети всіх операцій, визначених у Сі.


Необхідно пам'ятати, що всі оператори, окрім унарних і "?", пов'язують (приєднують,
асоціюють) свої операнди зліва направо. Унарні оператори (*, &, -) і "?" пов'язують
(приєднують, асоціюють) свої операнди справа наліво. У таблиці операції з вищим
пріоритетом розміщені у верхніх рядках і виконуються при обчисленні значення
будь-якого виразу в першу чергу, з більш низьким пріоритетом - під ними і
виконуються в другу чергу. Операції рівного пріоритету розміщені в одному рядку і
виконуються при обчисленні виразу зліва направо.
Таблиця 3.8. Повне зведення пріоритетів операцій

Найвищий пріоритет ( ) [ ] -> .


! ~ (- унарний) (++ інфіксний) (--інфіксний) (type) sizeof
* / %
+ -
<< >>
< <= > >=
== !=
&
^
|
&&
||
?:
= += -= *= /=
Найнижчий пріоритет ,

3.4. Вирази в мові Сі


Вирази складаються з операторів, констант, функцій і змінних. У мові Сі виразом
є будь-яка правильна послідовність цих елементів. Більшість виразів в мові Сі за
формою дуже схожі на алгебраїчні, часто їх і пишуть, керуючись правилами
алгебри. Однак тут необхідно бути уважним і враховувати специфіку виразів в мові
Сі.

Порядок обчислень

Порядок обчислення підвиразів в виразах мови Сі не визначено. Компілятор


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

х = f1() + f2();
- 37 -
немає ніяких гарантій того, що функція f1() буде викликана перед викликом f2().
Перетворення типів у виразах
Якщо у виразі зустрічаються змінні і константи різних типів, вони зводяться до
одного типу. Компілятор перетворює "менший" тип в "більший". Цей процес
називається просуванням типів. Спочатку всі змінні типів char і short int
автоматично просуваються в int. Це називається цілочисельним розширенням.
Після цього всі інші операції виконуються одна за одною, як описано в наведеному
нижче алгоритмі перетворення типів:

ЯКЩО операнд має тип long double ТО


другий операнд перетвориться в long double
ІНАКШЕ ЯКЩО операнд має тип double ТО
другий операнд перетвориться в double
ІНАКШЕ ЯКЩО операнд має тип float ТО
другий операнд перетвориться в float
ІНАКШЕ ЯКЩО операнд має тип unsigned long ТО
другий операнд перетвориться в unsigned long
ІНАКШЕ ЯКЩО операнд має тип long ТО
другий операнд перетвориться в long
ІНАКШЕ ЯКЩО операнд має тип unsigned int ТО
другий операнд перетвориться в unsigned int

Крім того, діє наступне правило: якщо один з операндів має тип long, а другий -
unsigned int, притому значення unsigned int не може бути представлено типом
long, то обидва операнда перетворюються в unsigned long.
Після виконання наведених вище перетворень обидва операнда відносяться до
одного і того ж типу, до цього типу належить і результат операції.
Розглянемо приклад перетворення типів, наведений на малюнку нижче.
char ch;
int i;
float f;
double d;
result = ( ch / i ) + ( f * d ) - ( f + i ) ;
| | | | | |
char int float double float int
|___ | |___| |____|
| | |
int double |
|__________| |
| |
double float
|_______________|
|
double
Спочатку символ ch перетворюється в ціле число. Результат операції ch / i
перетворюється в double, тому що результат f * d має тип double. Результат
операції f + i має тип float, тому що f має тип float. Остаточний результат має тип
double.
- 38 -

Дійсне перетворення типів: операція приведення типів

Програміст може "примусово" перетворити значення виразу до потрібного йому


типу, використовуючи операцію приведення типів. Загальна форма оператора
явного приведення типу:
(тип) вираз
Тут тип - це будь-який підтримуваний у мові Сі тип даних. Наприклад, такий
запис перетворює значення виразу х / 2 до типу float:

(float) х/2

Дійсне перетворення типу - це операція. Оператор приведення типу є унарним і


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

#include <stdio.h>
int main(void) { /* друк i та i/2 з дробовою частиною*/
int i;
for(i=1; i<=100; ++i)
printf("%d / 2 =: %f\n", i, (float) i /2);
return 0;
}

Без операції приведення (float) виконувалося б цілочисельне ділення. Дробова


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

Пробіли, круглі дужки та коментарі

Для підвищення зручності читання програми при запису виразів можна


використовувати пробіли і символи табуляції. Наприклад, наступні два оператора
еквівалентні:
x=10/y;
x = 10 / y;
Зайві дужки, якщо вони не змінюють пріоритет операцій, не призводять до
помилки і не уповільнюють обчислення виразу. Додаткові дужки часто
використовують для прояснення порядку обчислень. У наступному прикладі 2-й
рядок читається значно легше:

x = y/3-34*temp+127;
x = (y/3) - (34*temp) + 127;

Для запису коментаря в мові Сі використовуються подвійні символи:


- початок коментаря /*;
- кінець коментаря */
Якщо дія коментаря розповсюджується лише до кінця поточного рядку – можна
використовувати подвійний слеш, тобто //.
Вкладеність коментарів в мові Сі не допускається.
- 39 -

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

Оператор - це частина програми, яка може бути виконананою окремо. Це


означає, що оператор визначає деяку дію. У мові Сі існують такі групи операторів:
Умовні оператори
Оператори циклу
Оператори безумовного переходу
Оператори-вирази
Блоки
4.1. Умовні оператори
У мові Сі існують два умовних оператора: if і switch (слід пам'ятати, що при
певних обставинах операція ? є кращою альтернативою оператора if!).

Оператор if

Загальна форма оператора if наступна:

if (умовний_вираз)
оператор1;
else
оператор2;

Тут оператор може бути тільки одним оператором, блоком операторів або бути
відсутнім (порожній оператор). Фраза else може взагалі бути відсутньою.

Рис. 4.1. Робота оператора if…else

Якщо умовний_вираз істинний (тобто приймає будь-яке значення, відмінне від


нуля), то виконується оператор1 або блок операторів, наступний за умовою в if. В
іншому випадку виконується оператор2 (або блок операторів), наступний за else
(якщо ця фраза присутня). Необхідно пам'ятати, що виконується або оператор,
пов'язаний з if, або з else, але обидва - ніколи!
- 40 -
Якщо фраза else відсутня в операторі if він буде працювати наступним чином:

Рис. 4.2. Робота оператора if…


Такий оператор if носить назву редукованого і працює наступним чином. Якщо
значення умовного виразу є істинним буде виконуватися оператор чи блок
операторів, що стоїть після умови. Якщо значення умовного виразу є хибним ми
перейдемо зразу до виконання наступного оператора після if.
Умовний_вираз, що входить в if, повинен мати скалярний результат. Це
означає, що результатом має бути ціле число, символ, покажчик або число з
плаваючою точкою, але ним не може бути масив або структура. Слід уникати
використання в умовному_виразі оператора if операндів плаваючого типу, тому що
це істотно сповільнює обчислювальний процес. Пояснюється це тим, що для
виконання операцій над плаваючими операндами необхідно виконати більше
команд процесора, ніж для виконання операцій над цілими числами або символами.
Слід уникати використовувати в якості умови виразів з плаваючою точкою
наступного типу:

if (x==3.45) ….

тобто, "прямого" порівняння зі значенням змінної з плаваючою точкою. У цьому


випадку, через особливості представлення плаваючих чисел і обчислень з ними,
така умова має високі шанси ніколи не виконатися, тобто завжди залишатися
хибною! Для запобігання цього ефекту і за необхідності перевірити значення такої
змінної, необхідно перевіряти її потрапляння в певний інтервал, в середині якого
знаходиться порівнюване число. Розмір інтервалу вибирається програмістом
виходячи з умов точності.
У наступній програмі ілюструється використання оператора if. У ній
запрограмована тривіальна гра "вгадай число". Якщо гравець вгадав число, на
екран виводиться повідомлення ** Вірно **, в іншому випадку - ** Невірно **
Програма генерує випадкове число за допомогою стандартного генератора
випадкових чисел rand (). Генератор повертає випадкове число в діапазоні між 0 і
RAND_MAX (зазвичай це число не більше 32767). Функція rand () знаходиться в
заголовочному файлі <stdlib.h>.

/* Програма "Відгадай число" */


#include <stdio.h>
- 41 -
#include <stdlib.h>
int main(void) {
int magic; /* число */
int guess; /* спроба гравця*/
magic = rand(); /* генерація числа */
printf ("Відгадай число: ");
scanf ("%d", &guess);
if (guess == magic)
printf ("** Вірно **");
else
printf ("Невірно");
return 0;
}

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


оператора if або else. У практиці програмування вкладені умовні оператори
використовуються доволі часто. У вкладеному умовному операторі фраза else
завжди асоціюється з найближчим if в тому ж блоці, якщо цей if не асоційований з
іншого фразою else. наприклад:

if (i) {
if (j) оператор1;
if (k) оператор2; /* цей if */
else оператор3; /* асоційований із цим else */
}
else оператор4; /* асоційований із if(i) */

Остання фраза else не асоційована з if (j) тому, що вона знаходиться в іншому


блоці. Ця фраза else асоційована з if (i). Внутрішня фраза else асоційована з if (k),
тому що цей if - найближчий.

Рис. 4.3. Робота вкладеного оператора if-…-else

Стандарт С89 допускає 15 рівнів вкладеності умовних операторів, а більшість


компіляторів допускають значно більшу кількість рівнів вкладеності. Однак на
практиці необхідність в глибині вкладеності, більшою, ніж кілька рівнів, виникає
досить рідко, так як збільшення глибини вкладення швидко заплутує програму і
робить її складною для розуміння.
- 42 -
У наступному прикладі вкладений оператор if використовується в
модернізованій програмі для гри в число. З його допомогою гравець отримує
повідомлення про характер помилки:

/* Вгадай число!, програма № 2. */


#include <stdio.h>
#include <stdlib.h>
int main(void) {
int magic; /* число */
int guess; /* спроба гравця*/
magic = rand(); /* генерація числа */
printf("Вгадай число: ");
scanf("%d", &guess);
if (guess == magic) {
printf("** Вірно **");
printf("Число дорівнює %d\n", magic);
}
else {
printf("** Невірно, ");

/* вкладений if */
if(guess > magic)
printf("надто велике\n");
else
printf("надто мале\n");
}
return 0;
}

У програмах часто використовується конструкція, яку називають "драбинкою" if-


else-if. Загальна форма драбинки має вигляд:

if (умова1)
оператор1;
else if (умова2)
оператор2;
else if (умова3)
оператор3;
.
.
.
else операторN;

Працює ця конструкція наступним чином. Умовні вирази операторів if


обчислюються зверху вниз. Після виконання деякої умови, тобто коли зустрінеться
вираз, що приймає значення ІСТИННОГО, виконується асоційований з цим виразом
оператор, а решта «драбинки» пропускається. Якщо всі умови помилкові, то
виконується оператор в останній фразі else, а якщо остання фраза else відсутня, то
в цьому випадку не виконується жоден оператор.
- 43 -

Операція "?" ( альтернативна умовному оператору)

Операцію ? можна використовувати замість оператора if-else, записаного у


формі

if (умова) змінна = вираз2;


else змінна = вираз3;

Операція ? є тернарною, тому що вона має три операнда. Її загальна форма


наступна:

Умовний_вираз1 ? Вираз2 : Вираз3;

Результат операції ? визначається наступним чином. Спочатку обчислюється


Умовний_вираз1. Якщо він має значення ІСТИННОГО, обчислюється Вираз2 і його
значення стає результатом операції ?. Якщо Умовний_вираз1 має значення
ХИБНОГО, обчислюється Вираз3 і його значення стає результатом операції ?.
Наприклад:

x = 10;
y = x>9 ? 100 : 200;

У цьому прикладі змінній y присвоюється значення 100. Якби x було менше 9, то


змінна y отримала б значення 200. Те ж саме можна записати, використовуючи
оператор if-else:

x = 10;
if (x>9)
y = 100;
else
y = 200;

Використовуючи оператор ?, програму для гри в "число" можна переписати


таким чином:

/ * Вгадай число, програма № 3. * /


#include <stdio.h>
#include <stdlib.h>
int main (void) {
int magic; / * Магічне число * /
int guess; / * Спроба гравця * /
magic = rand (); / * Генерація магічного числа * /
printf ("Вгадай число:");
scanf ("% d", & guess);
if (guess == magic) {
printf ("** Вірно **");
printf ("Число дорівнює% d \ n", magic);
}
else
guess> magic? printf ("Занадто велике") : printf ("Занадто мале");
return 0;
}
- 44 -
У цій програмі оператор ? друкує відповідне повідомлення на основі перевірки
умови guess > magic.

Умовний вираз

У початківців іноді виникають труднощі у зв'язку з тим, що в умовному


(керуючому) виразі операторів if або ? можуть стояти будь-які оператори, причому
це не обов'язково оператори відношень або логічні (як в мовах Basic або Pascal). У
мові Сі значенням результату умовного виразу є ІСТИННЕ або ХИБНЕ, проте тип
результату може бути будь-яким скалярним типом. Вважається, що будь-який
ненульовий результат представляє значення ІСТИННОГО, а нульовий - ХИБНОГО.
У наступному прикладі програма зчитує з клавіатури два числа, обчислює їх
відношення і виводить його на екран. Оператор if використовується для того, щоб
уникнути поділу на нуль, якщо друге число дорівнює нулю.

/* Ділення першого числа на друге. */


#include <stdio.h>
int main(void) {
int a, b;
printf("Введіть два числа: ");
scanf("%d%d", &a, &b);
if (b)
printf("%d\n", a/b);
else
printf("Ділити на нуль не можна!.\n");
return 0;
}

Якщо керуючий вираз b дорівнює 0, то його результат становить значення


ХИБНОГО і виконується оператор else. В іншому випадку (b не дорівнює нулю)
результат представляє значення ІСТИННОГО і виконується ділення чисел.
В останньому прикладі оператор if можна записати і таким чином:

if (b != 0) printf("%d\n", a/b);

Але слід зазначити, що така форма запису надлишкова, вона може привести до
генерації неоптимального коду, крім того, це вважається ознакою поганого стилю.
Змінна b сама по собі представляє значення ІСТИННОГО або ХИБНОГО, тому
порівнювати її з нулем немає необхідності.
4.2. Оператор вибору - switch
Оператор вибору switch (часто його називають перемикачем) призначений для
вибору гілки обчислювального процесу, виходячи зі значення керуючого виразу.
(При цьому значення керуючого виразу порівнюється зі значеннями в списку цілих
або символьних констант. Якщо буде знайдено збіг, то виконається асоційований з
такою константою оператор.) Загальна форма оператора switch наступна:

switch (вираз) {
case константа1:
послідовність операторів
break;
case константа2:
- 45 -
послідовність операторів
break;
case константа3:
послідовність операторів
break;
default:
послідовність операторів;
}

Значення виразу оператора switch має бути таким, щоб його можна було
виразити цілим числом, або символом. Це означає, що в керуючому виразі можна
використовувати лише змінні цілого або символьного типу, але тільки не з
плаваючою точкою. Значення керуючого виразу по черзі порівнюється з
константами в операторах case. Якщо значення керуючого виразу співпаде з якоюсь
із констант, управління передається на відповідну мітку case і виконується
послідовність операторів до оператора break. Якщо оператор break відсутній,
виконання послідовності операторів триває до тих пір, поки не зустрінеться break (в
блоці іншої мітки), або не скінчиться тіло оператора switch (тобто, не почнеться
блок, наступний за switch). Мітка default виконується в тому випадку, коли значення
керуючого виразу не співпало з жодною константою. Блок default також може бути
відсутнім. В цьому випадку за відсутності збігів не виконується жоден оператор.

Рис. 4.4. Робота оператора switch

Згідно зі Стандартом С89, оператор switch може мати максимум 256 операторів
case. Оператор case не може бути використаний сам по собі, поза оператора
switch.
Оператор break - це один з операторів безумовного переходу. Він може
застосовуватися не тільки в операторі switch, але і в циклах. Коли в тілі оператора
switch зустрічається оператор break, програма виходить з оператора switch і
виконує оператор, наступний за кінцевою фігурною дужкою } оператора switch.
Про оператор switch дуже важливо пам'ятати наступне:
 оператор switch відрізняється від if тим, що в ньому керуючий вираз
перевіряється тільки на рівність константі, в той час як в if може
перевірятися будь-який вид відношення або логічного виразу;
 в одному і тому ж операторі switch ніякі два оператори case не можуть мати
однакові константи. Звичайно, якщо один switch вкладений в інший, то в
таких операторах case можуть бути і константи, що збігаються;
 якщо в керуючому виразі оператора switch зустрічаються символьні
константи, вони автоматично перетворюються в цілий тип за прийнятими в
мові Сі правилами приведення типів.
- 46 -
Оператор switch часто використовується для обробки команд з клавіатури,
наприклад, при виборі пунктів меню. У наступному прикладі програма виводить на
екран меню перевірки правопису і викликає відповідну функцію:

void menu(void) {
char ch;
printf ("1. Перевірка правопису \ n");
printf ("2. Корекція помилок \ n");
printf ("3. Вивід помилок \ n");
printf ("Для пропуску натисніть будь-яку клавішу \ n");
printf ("Введіть Ваш вибір:");
ch = getch(); / * Зчитування клавіш * /
switch (ch) {
case '1':
check_spelling ();
break;
case '2':
correct_errors ();
break;
case '3':
display_errors ();
break;
default:
printf ("Не обрана жодна опція");
}
}

З точки зору синтаксису, присутність операторів break всередині switch не


обов'язкова. Вони переривають виконання послідовності операторів, асоційованих
з даної константою. Якщо оператор break буде відсутній для відповідного case, то
виконується наступний оператор case, поки не зустрінеться черговий break, або не
буде досягнутий кінець тіла оператора switch.
Таким чином є наступні дві особливості оператора switch():
 по-перше, оператор case може не мати асоційованої з ним послідовності
операторів. Тоді управління зразу переходить до наступного case;
 по-друге, якщо оператор break в поточному case відсутній, то виконується
послідовність операторів наступного case.

Вкладені оператори switch

Оператор switch у свою чергу, так само, як і оператор if, може перебувати в тілі
зовнішнього по відношенню до нього оператора switch. Оператори case
внутрішнього і зовнішнього switch можуть мати однакові константи, в цьому випадку
вони не конфліктують між собою. Наприклад, наступний фрагмент програми цілком
роботоздатний:

switch (x) {
case 1:
switch (y) {
case 0: printf ("Ділення на нуль. \ n");
break;
case 1: process (x, y);
- 47 -
break;
}
break;
case 2: <оператори>;
}

4.3. Блок операторів


Блок - це послідовність операторів, взятих у фігурні дужки, що розглядається,
як одна програмна одиниця. Оператори, що становлять блок, логічно пов'язані один
з одним. Іноді блок називають складеним оператором. Блок завжди починається
відкритою фігурною дужкою { і закінчується закритою }. Найчастіше блок
використовується як складова частина будь-якого оператора, що виконує дію над
групою операторів, наприклад, if або for. Однак блок можна поставити в будь-якому
місці, де може перебувати оператор, як показано в наступному прикладі:

#include <stdio.h>
int main(void) {
int i;
{ /* блок операторів */
i = 120;
printf("%d", i);
}
return 0;
}

4.4 Оператор циклу for


Загальна форма оператора for така:

for (ініціалізація_параметра_циклу; умова_виконання_циклу;


зміна_параметра_циклу)
оператор;

Рис. 4.5. Зображення циклу for на схемі алгоритму

У найбільш загальному вигляді принцип його роботи наступний. Ініціалізація -


це присвоєння початкового значення змінній, яка називається параметром циклу,
або лічильником циклу. Умова являє собою умовний вираз, що визначає, чи слід
виконувати поточну ітерацію циклу (яка часто називається тілом циклу) в черговий
раз. Секція зміни_параметра здійснює зміну параметра циклу по відповідному
- 48 -
алгоритму при кожній ітерації. Ці три частини (вони називаються секціями
оператора for) обов'язково розділяються крапкою з комою. Цикл for виконується,
якщо вираз умова приймає значення ІСТИНИ. Якщо умовний вираз секції 2 хоча б
один раз прийме значення ХИБНОГО, то програма виходить з циклу і виконується
оператор, наступний за тілом цикла for.
У наступному прикладі в циклі for виводяться на екран числа від 1 до 100:

#include <stdio.h>
int main(void) {
int x;
for(x=1; x <= 100; x++)
printf("%d ", x);
return 0;
}

У цьому прикладі параметр циклу х ініціалізований числом 1, а потім при кожній


ітерації порівнюється з числом 100. Поки змінна х менше 100, викликається функція
printf () і цикл повторюється. При цьому х збільшується на 1 і знову перевіряється
умова циклу х <= 100. Процес повторюється, поки змінна х не стане більше 100.
Після цього процес виходить з циклу, а управління передається оператору,
наступному за циклом. У цьому прикладі параметром циклу є змінна х, при кожній
ітерації вона змінюється і перевіряється в секції умови циклу.
У наступному прикладі в циклі for виконується блок операторів:

for (x = 100; x! = 65; x - = 5) {


z = x * x;
printf ( "Квадрат %d дорівнює %d", x, z);
}

Операції зведення змінної х в квадрат і виклику функції printf() повторюються, поки


х не прийме значення 65. Тут параметр циклу зменшується, він ініціалізований
числом 100 і зменшується на 5 при кожній ітерації.
В операторі for умова циклу завжди перевіряється перед початком ітерації. Це
означає, що оператори циклу можуть не виконуватися жодного разу, якщо перед
першою ітерацією умова зразу прийме значення ХИБНОГО. Наприклад, в
наступному фрагменті програми:

x = 10;
for (y=10; y!=x; ++y)
printf("%d", y);
printf("%d", y); /* Це єдиний printf()
котрий буде виконано */

цикл не виконається жодного разу, тому що при вході в цикл значення змінних х і у
рівні. Тому умова циклу приймає значення ХИБНОГО, а тіло циклу і оператор
інкремента не виконуються. Змінна у залишається рівною 10, єдиний результат
роботи цієї програми - вивід на екран числа 10 в результаті виклику функції printf(),
розташованої поза циклом.
- 49 -

Варіанти циклу for

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


випадках збільшити ефективність і гнучкість програми.
Один з поширених способів посилення ефективності циклу for - застосування
операції "кома" для створення двох параметрів циклу. Операція "кома" пов'язує
кілька виразів, змушуючи їх виконуватися разом. У наступному прикладі обидві
змінні (х і у) є параметрами циклу for і обидві ініціалізуються прямо в цьому циклі:

for (x=0, y=0; x+y<10; ++x) {


y = getch();
y = y - '0'; // Віднімання від ASCII-коду символа в y числового коду символа '0'
.
.
.
}
Тут кома розділяє дві операції ініціалізації. При кожній ітерації значення змінної
х збільшується, а значення у вводиться з клавіатури. Для виконання ітерації як х,
так і у повинні мати певне значення. Незважаючи на те, що значення у вводиться з
клавіатури, воно повинно бути ініціалізовано таким чином, щоб виконалась умова
циклу при першій ітерації. Якщо у не ініціалізувати, то воно може випадково
виявитися таким, що умова циклу зразу, ще до вводу значення y, прийме значення
ХИБНОГО, і тіло циклу не буде виконано жодного разу.
Приклад практичного використання двох керуючих змінних в циклі for.
Розглянемо програму, яка знаходить найбільший і найменший множники числа (в
даному випадку числа 100). Зверніть особливу увагу на умову завершення циклу:
вона включає обидві керуючих змінних.

/*
Використання операції "кома" в секціях заголовка циклу for для визначення
найбільшого і найменшого множників числа.
*/
#include <stdio.h>
int main(void) {
int i, j;
int smallest, largest;
int num;
num = 100;
smallest = largest = 1;
for(i=2, j=num/2; (i <= num/2) && (j >= 2); i++, j--) {
if((smallest == 1) && ((num % i) == 0))
smallest = i;
if((largest == 1) && ((num % j) == 0))
largest = j;
}
printf ("Найбільший множник: %d", largest);
printf ("Найменший множник: %d", smallest);
}

Результати виконання:
Найбільший множник: 50
Найменший множник: 2
- 50 -

Завдяки використанню відразу двох керуючих змінних в одному циклі for можна
знайти як найбільший, так і найменший множник числа. Для визначення найменшого
множника використовується керуюча змінна i. Спочатку вона встановлюється
рівною числу 2 і інкрементується до тих пір, поки її значення не перевищить
половину досліджуваного числа (воно зберігається в змінній num). Для визначення
найбільшого множника використовується керуюча змінна j. Спочатку вона
встановлюється рівною половині числа, що зберігається в змінній num, і
декрементується до тих пір, поки її значення не стане меншим 2. Цикл працює до
тих пір, поки обидві змінні i і j, не досягнуть своїх кінцевих значень. По завершенню
циклу будуть знайдені обидва множники.
Перевірка параметра циклу на відповідність деякій умові необов'язкова. Умова
може бути будь-яким логічним оператором або оператором відношення. Це
означає, що умова виконання циклу може складатися з декількох умов, або
операторів відношення. Наступний приклад демонструє застосування складеної
умови циклу для перевірки пароля, що вводиться користувачем. Користувачеві
надаються три спроби введення пароля. Програма виходить з циклу, коли
використані всі три спроби або коли введений вірний пароль.

#include <string.h>
void sign_on(void)
{
char str[20];
int x;
for (x=0; x<3 && strcmp(str, "пароль"); ++x) {
printf("Будь ласка, введіть пароль:");
gets(str);
}

if (x==3)
return;
/* Інакше користувач допускається*/
.
.
.
}

Функція sign_on() використовує стандартну бібліотечну функцію strcmp(), яка


порівнює два рядки і повертає 0, якщо вони збігаються.
Слід пам'ятати, що кожна з трьох секцій оператора for може бути будь-яким
синтаксично правильним виразом. Ці вирази не завжди якимось чином можуть
відображати призначення секції. Розглянемо наступний приклад:

#include <stdio.h>
int sqrnum(int num);
int readnum(void);
int prompt(void);

int main(void)
{
int t;
for(prompt(); t=readnum(); prompt())
- 51 -
sqrnum(t);
return 0;
}

int prompt(void)
{
printf("Введіть число: ");
return 0;
}

int readnum(void)
{
int t;
scanf("%d", &t);
return t;
}

int sqrnum(int num)


{
printf("%d\n", num*num);
return num*num;
}

Тут в main() кожна секція циклу for складається з викликів функцій, які
пропонують користувачеві ввести число і зчитують його. Якщо користувач ввів 0, то
цикл припиняється тому, що тоді умова циклу приймає значення ХИБНОГО. В
іншому випадку число зводиться до квадрату. Таким чином, в цьому прикладі циклу
for секції ініціалізації і збільшення використовуються незвично, але абсолютно
правильно.
Інша цікава особливість циклу for полягає в тому, що його секції можуть бути
взагалі порожніми, присутність в них будь-якого виразу не є обов'язковою. У
наступному прикладі цикл виконується, поки користувач не введе число 123:

for (x = 0; x! = 123;)
scanf ( "% d", &x);

Секція зміни параметру циклу for тут залишена порожньою. Це означає, що перед
кожною ітерацією значення змінної х перевіряється на відмінність від числа 123, а
збільшення не відбувається, воно тут непотрібне. Якщо з клавіатури ввести число
123, то умова приймає значення ХИБНОГО і програма виходить із циклу.
Ініціалізацію параметра циклу for можна зробити за межами цього циклу, але,
звичайно, до нього. Це особливо доречно, якщо початкове значення параметра
циклу обчислюється досить складно, наприклад:

gets(s); /* читає рядок в s */


if (*s)
x = strlen(s); /* вираховується довжина рядка*/
else
x = 10;
for( ; x<10; ) {
printf("%d", x);
++x;
- 52 -
}
У цьому прикладі секція ініціалізації залишена порожньою, а змінна х ініціалізується
до входу в цикл.

Нескінченний цикл

Для створення нескінченного циклу можна використовувати будь-який оператор


циклу, але частіше за все для цього вибирають оператор for. Так як в операторі for
може бути відсутньою будь-яка секція, нескінченний цикл найпростіше зробити,
залишивши порожніми всі секції, як показано в наступному прикладі:

for (;;)
printf ("Цей цикл повторюється нескінченно. \ n");

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


ІСТИННЕ. У оператор for можна додати вирази в секції ініціалізації і зміни
параметра, хоча зазвичай для створення нескінченного циклу використовують
конструкцію for (;;).
Фактично конструкція for (;;) ще не гарантує нескінченність ітерацій, тому що в
тілі самого циклу може зустрітися оператор break, що викликає негайний вихід з
циклу. У цьому випадку виконання програми продовжується з наступного за циклом
for оператора програми:

ch = '\0';
for( ; ; ) {
ch = getch(); /* зчитування символу */
if (ch=='A')
break; /* вихід із циклу */
}
printf("Ви надрукували 'A' “);
В данному прикладі цикл виконується до тих пір, поки користувач не введе з
клавіатуры символ 'А'.

Цикл for без тіла циклу

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


означає, що тіло циклу for (або будь-якого іншого циклу) також може бути порожнім.
Таку особливість циклу for можна використовувати для спрощення деяких програм,
а також в циклах, призначених для того, щоб призупинити виконання наступної
частини програми на деякий час.
Програмісту іноді доводиться вирішувати задачу видалення пробілів з
вхідного потоку. Припустимо, необхідно видалити всі початкові пробіли в рядку. У
наступному прикладі цикл for видаляє початкові пробіли в рядку str:

for (; *str == ' '; str++);

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


пробілом. Цикл не має тіла, так як в ньому немає необхідності – вся робота з пошуку
та видалення пробілів виконується у заголовку самого цикла.
Іноді виникає необхідність відкласти виконання наступної частини програми на
певний час. Це можна зробити за допомогою циклу for таким чином:
- 53 -
for (t = 0; t < SOME_VALUE; t++);

Єдине призначення цього циклу - затримка виконання наступної частини програми.


Однак слід мати на увазі, що компілятор може оптимізувати об'єктний код таким
чином, що пропустить такий цикл взагалі, оскільки він не виконує ніяких дій, тоді
бажана затримка виконання наступної частини програми не відбудеться.
4.5. Цикл while
Загальна форма цикла while має наступний вигляд:

while (умова) оператор;

Тут оператор (тіло циклу) може бути порожнім оператором, єдиним оператором або
блоком операторів. Умова (керуючий вираз) може бути будь-яким допустимим в
мові Сі виразом. Умова вважається істинною, якщо значення виразу не дорівнює
нулю, а оператор циклу виконується, якщо умова приймає значення ІСТИННОГО.
Якщо умова приймає значення ХИБНОГО, програма виходить з циклу і виконується
наступний за циклом оператор.

Рис. 4.6. Зображення циклу while на схемі алгоритма

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


користувач не введе символ 'А':

char wait_for_char(void)
{
char ch;
ch = '\0'; /* ініціалізація ch */
while(ch != 'A')
ch = getch();
return ch;
}

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


значення ch ініціалізується нулем. Умова циклу while істинна, якщо ch не дорівнює
- 54 -
'А'. Оскільки ch ініціалізована нулем, умова істинна і цикл починає виконуватися.
Умова перевіряється при кожному натисканні клавіші користувачем. При введенні
символу 'А' умова стає хибною і виконання циклу припиняється.
Як і в циклі for, в циклі while умова перевіряється перед початком ітерації. Це
означає, що якщо умова помилкова, тіло циклу не буде виконано. Завдяки цьому
немає необхідності вводити в програму окрему умову перед циклом. Розглянемо це
на прикладі функції pad(), яка додає пробіли в кінець рядка і робить довжину рядка
рівною наперед заданій величині. Якщо рядок вже має необхідну довжину, то
пробіли взагалі не додаються:

#include <stdio.h>
#include <string.h>

void pad(char *s, int length);

int main(void) {
char str[80];
strcpy(str, "це перевірка");
pad(str, 40);
printf("%d", strlen(str));
return 0;
}

/* Додання пробілів в кінець рядка. */


void pad(char *s, int length) {
int l;
l = strlen(s); /* визначення довжини рядка*/
while(l < length) {
s[l] = ' '; /* вставка пробілів */
l++;
}
s[l]= '\0'; /* рядок повинен закінчуватися нулем */
}

Аргументами функції pad() є s (покажчик на вихідний рядок) і length (необхідна


кількість символів в рядку). Якщо довжина рядка s при вході в функцію дорівнює або
більше length, то цикл while не виконується. В іншому випадку pad() додає необхідну
кількість пробілів, а бібліотечна функція strlen() повертає довжину рядка.
Якщо виконання циклу має залежати від декількох умов, можна створити так
звану керуючу змінну, значення якій присвоюються різними операторами тіла циклу.
Розглянемо наступний приклад:
void func1(void) {
int working;

working = 1; /* тобто ІСТИННЕ */

while(working) {
working = process1();
if(working)
working = process2();
if(working)
working = process3();
- 55 -
}
}

У цьому прикладі змінна working є керуючою. Будь-яка з трьох функцій може


повернути значення 0 і цим перервати виконання циклу. Тіло циклу while може бути
так само порожнім. Наприклад, цикл
while ((ch = getch())! = 'A');
виконується до тих пір, поки користувач не введе символ 'А'. Тут оператор
присвоювання виконує два завдання: присвоює значення правого виразу змінній ch
зліва і повертає це значення як значення лівого операнду операції порівняння !=.

4.6. Цикл do-while


На відміну від циклів for і while, які перевіряють свою умову перед ітерацією,
do-while робить це після виконання ітерації циклу. Тому цикл do-while ЗАВЖДИ
виконується хоч один раз:

do {
оператор;
} while (умова);

Якщо оператор не є блоком, фігурні дужки необов'язкові, але їх майже завжди


ставлять, щоб оператор досить наочно відокремлювався від умови. Ітерації
оператора do-while виконуються, поки умова не прийме значення ХИБНЕ.

Рис. 4.7. Зображення циклу do while на схемі алгоритму

У наступному прикладі в циклі do-while числа зчитуються з клавіатури, поки не


зустрінеться число, менше або рівне 100:

do {
scanf ("%d", &num);
} while (num > 100);

Цикл do-while часто використовується у функціях вибору пунктів меню. Якщо


користувач вводить допустиме значення, воно повертається в якості значення
- 56 -
функції. В іншому випадку цикл вимагає повторити введення. Наступний приклад
демонструє версію програми для вибору пункту меню перевірки граматики:

void menu(void) {
char ch;
printf ("1. Перевірка правопису \ n");
printf ("2. Корекція помилок \ n");
printf ("3. Вивід помилок \ n");
printf ("Введіть Ваш вибір:");

do {
ch = getch(); / * Читання вибору з клавіатури * /
switch(ch) {
case '1':
check_spelling();
break;
case '2':
correct_errors();
break;
case '3':
display_errors();
break;
}
} while(ch!='1' && ch!='2' && ch!='3');
}
У цьому прикладі застосування циклу do-while вельми доречне, тому що ітерація
тут завжди повинна виконатися, як мінімум один раз. Цикл повторюється, поки його
умова не стане хибною, тобто поки користувач не введе одну з допустимих
відповідей.
4.7. Оператори переходу
У мові Сі визначені чотири оператора переходу: return, goto, break і continue.
Оператори return і goto можна використовувати в будь-якому місці всередині
функції. Оператори break і continue можна використовувати в будь-якому з
операторів циклу. Крім того, break можна також використовувати в операторі
switch.

Оператор return

Оператор return використовується для виходу з функції. Віднесення його до


категорії операторів переходу обумовлено тим, що він змушує програму перейти на
оператор програми, який слідує за оператором виклику функції (тобто, в функцію,
що викликала). Оператор return може мати асоційоване з ним справа значення, тоді
при виконанні даного оператора це значення повертається в якості значення
функції. У функціях типу void ЗАВЖДИ використовується оператор return без
значення.
Загальна форма оператора return наступна:

return вираз;

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


значення виразу стає тим значенням, що повертає функція у своєму імені.
- 57 -
Усередині функції може бути присутнім будь-яка кількість операторів return.
Вихід з функції відбувається тоді, коли зустрічається один з них. Закриваюча
фігурна дужка } у кінці тіла функції також викликає вихід з функції. Вихід програми
на неї еквівалентний виконанню оператора return без значення. У цьому випадку
функція, тип якої відмінний від void, повертає невизначене значення.
Функція, визначена зі специфікатором void, не може містити return зі
значенням, так, як ця функція не повертає значення.

Оператор goto

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


міткою після нього.
Слід сказати, що окрім goto, в мові Сі є інші оператори керування (наприклад
break, continue), тому необхідності в застосуванні goto практично немає (за
винятком виходу з вкладених циклів). В результаті надмірного використання
операторів goto програма погано читається, вона стає схожою на «спагетті».
Вважається, що в програмуванні не існує ситуацій, в яких не можна обійтися без
оператора goto. Але в деяких випадках його застосування все ж доречно. Іноді, при
вмілому використанні, цей оператор може виявитися вельми корисним, наприклад,
якщо потрібно вийти з глибоко вкладених циклів.
Для оператора goto завжди необхідна мітка. Мітка - це ідентифікатор з
наступною двокрапкою. Мітка повинна знаходитися в тій самій функції, що і goto,
перехід в іншу функцію неможливий. Загальна форма оператора goto виглядає так:

goto мітка;
частина програми 1
.
мітка:
частина програми 2
Мітка може знаходитися як до, так і після оператора goto. Наприклад,
використовуючи оператор goto, можна виконати цикл від 1 до 100 таким чином:
x = 1;
loop1:
x++;
if(x < =100)
goto loop1;

Оператор break

Оператор break застосовується в двох випадках. По-перше, в операторі switch


з його допомогою переривається виконання послідовності case. В цьому випадку
оператор break передає управління за межі блоку switch. По-друге, оператор break
використовується для негайного припинення виконання циклу без перевірки його
умови, в цьому випадку оператор break передає управління оператору, наступному
після оператора циклу.
Коли всередині циклу зустрічається оператор break, виконання циклу
безумовно (тобто без перевірки будь-яких умов) припиняється і керування
передається оператору, наступному за оператором цикла. Наприклад, програма

#include <stdio.h>
int main(void)
{
- 58 -
int t;
for(t=0; t<100; t++) {
printf("%d ", t);
if(t==10)
break;
}
return 0;
}
виводить на екран числа від 0 до 10. Після цього виконання циклу припиняється
оператором break, умова t <100 при цьому ігнорується.
Оператор break часто використовується в циклах, в яких деяка подія має
викликати негайне припинення виконання циклу.
Оператор break викликає вихід тільки з внутрішнього циклу. Наприклад,
програма

for (t = 0; t <100; ++ t) {
count = 1;
for (;;) {
printf ("% d", count);
count ++;
if (count == 10)
break;
}
}

100 разів виводить на екран числа від 1 до 9. Оператор break передає управління
зовнішньому циклу for.
У разі виникнення необхідності виходу з усіх вкладених циклів відразу, слід
використовувати замість break оператор безумовного переходу goto. Робиться це
в такий спосіб (перепишемо попередній приклад):
.
.
.
for(t=0; t<100; ++t) {
count = 1;
for(;;) {
printf("%d ", count);
count++;
if(count==10)
goto EndCycle;
}
}

EndCycle:
printf ("%d", count+1);
.
.
.

Якщо оператор break присутній всередині оператора switch, який вкладений


в будь-які цикли, то break відноситься тільки до switch, виходу із циклу не
відбувається.
- 59 -

Функція exit()

Функція exit() не є оператором мови, проте розглянемо можливість її


застосування. Аналогічно припинення виконання циклу оператором break, можна
припинити роботу усієї програми і за допомогою виклику стандартної бібліотечної
функції exit(). Ця функція викликає негайне припинення роботи ВСІЄЇ програми і
передає управління операційній системі.
Загальна форма функції exit() виглядає так:

void exit (int код_повернення);

Значення змінної код_повернення передається процесу операційної системи, що


викликав програму. Нульове значення коду повернення зазвичай використовується
для вказівки про нормальне завершення роботи програми. Інші значення вказують
на характер помилки. Функція exit() оголошена в заголовочному файлі <stdlib.h>.
Функція exit() часто використовується як для нормального завершення роботи
програми з нульовим кодом, так і коли програма в силу якіхось причин не може
виконуватися далі (наприклад, виникли помилки, які програма не може
відкорригувати).

Оператор continue

Оператор continue трохи схожий на break. Різниця між ними полягає в тому,
що оператор break викликає переривання виконання ВСЬОГО циклу, a оператор
continue – лише переривання виконання поточної ітерації циклу і здійснює перехід
до наступної ітерації. При цьому всі оператори до кінця тіла циклу пропускаються. У
циклі for оператор continue викликає виконання секції зміни керуючої змінної циклу
і наступної перевірки умови виконання циклу. У циклах while і do-while оператор
continue зразу викликає виконання умови перевірки продовження циклу.
У наступному прикладі оператор continue застосовується для виходу з циклу
while шляхом передачі управління на заголовок циклу:

void code(void) {
char done, ch;
done = 0;
while(!done) {
ch = getch();
if (ch=='$') {
done = 1;
continue;
}
putchar(ch+1); /* друк закодованого символа */
}
}
Функція code призначена для кодування повідомлення шляхом заміни кожного
введеного символу символом, код якого на 1 більше коду вхідного символу в коді
ASCII. Наприклад, символ 'А' замінюється символом 'В' (якщо це латинські
символи.). Функція припиняє роботу при введенні символу '$'. При цьому змінній
done присвоюється значення 1 і оператор continue передає управління на
заголовок цикла, де перевіряється умова, після чого цикл і завершується.
- 60 -

ТЕМА 5. РОБОТА З МАСИВАМИ ТА РЯДКАМИ

Масив - це набір змінних одного типу, що мають одне і те ж ім'я. Доступ до


конкретного елементу масиву здійснюється за допомогою індексу. У мові Сі всі
елементи масиву розташовуються в окремій неперервній області пам'яті. Перший
елемент масиву розташовується за найменшою адресою, а останній - за
найбільшою. Масиви можуть бути одновимірними і багатовимірними. Рядок являє
собою масив символьних змінних, що закінчується спеціальним нульовим символом
'\0'.
5.1. Одновимірні масиви
Загальна форма оголошення одновимірного масиву має наступний вигляд:

тип ім'я_змінної [розмір];

Як і інші змінні, масив повинен бути оголошений явно, щоб компілятор виділив для
нього певну область пам'яті (тобто розмістив масив). Тут тип позначає базовий тип
масиву, який є типом кожного елемента. Розмір задає кількість елементів масиву.
Наприклад, наступний оператор оголошує масив з 100 елементів типу double під
ім'ям balance:

double balance [100];

Відповідно до стандарту Сі розмір масиву повинен бути вказаний явно за


допомогою виразу-константи. Таким чином, в програмі на Сі розмір простого масиву
визначається під час компіляції і згодом залишається незмінним.
Доступ до елемента масиву здійснюється за допомогою імені масиву і
індексу. Індекс елемента масиву вказується в квадратних дужках після імені.
Наприклад, оператор

balance [3] = 12.23;

присвоює 4-му елементу масиву balance значення 12.23.


Індекс першого елемента будь-якого масиву в мові Сі дорівнює нулю. Тому
оператор

char p [10];

оголошує масив символів з 10 елементів - від р[0] до р[9]. У наступній програмі


обчислюються значення елементів масиву цілого типу з індексами від 0 до 99:

#include <stdio.h>
int main(void) {
int x[100]; /* оголошення масиву 100 цілих */
int t;

/* присвоєння елементам масиву значень від 0 до 99 * /


for(t=0; t<100; ++t)
x[t] = t;

/* вивід на екран вмісту масиву x */


for(t=0; t<100; ++t)
- 61 -
printf("%d ", x[t]);

return 0;
}

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


його типом і розміром. Для одновимірного масиву кількість байтів пам'яті
обчислюється таким чином:

кількість_байтів = sizeof(базовий_тип) × довжина_масиву

Під час виконання програми на Сі (на відміну від деяких інших мов
програмування) ніколи автоматично не перевіряється ні дотримання меж масивів,
ні їх вміст. В область пам'яті, зайняту масивом, може бути записано що завгодно,
навіть програмний код. Програміст повинен сам (де це необхідно) ввести
перевірку меж індексів.
Наступний приклад програми компілюється без помилок, однак при виконанні
відбувається порушення границі масиву count та руйнування сусідніх комірок
пам'яті:

int count[10], i;
/* тут порушена границя масиву count */
for(i=0; i<100; i++)
count[i] = i;

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


неперервній області пам'яті в порядку індексації. Нижче показано, як зберігається в
пам'яті масив а, що починається за адресою 1000 і оголошений як

char a[7];

Элемент a[0] a[1] a[2] a[3] a[4] a[5] a[6]


Адреса 1000 1001 1002 1003 1004 1005 1006

5.2. Рядки
Одновимірний масив найбільш часто застосовується у вигляді рядка символів.
Рядок - це одновимірний масив символів, що закінчується нульовим символом. У
мові Сі ознакою закінчення рядка (нульовим символом) служить символ '\0'. Таким
чином, рядок містить символи, що становлять рядок, а також кінцевий нульовий
символ. Це єдиний вид рядків, визначений в Сі.
Оголошуючи масив символів, призначений для зберігання рядка, необхідно
передбачити місце для нуля, тобто вказати його розмір в оголошенні на один
символ більше, ніж найбільша передбачувана кількість символів. Наприклад,
оголошення масиву str, призначеного для зберігання рядка з 10 символів, має
виглядати так:

char str[11];

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


- 62 -
Записаний в тексті програми рядок символів, взятих в подвійні лапки, є
рядковою константою, наприклад,
"Деякий рядок"
В кінець рядкової константи компілятор автоматично додає нульовий символ.
Для обробки рядків в Сі визначено багато різних бібліотечних функцій. Найчастіше
використовуються такі функції:

Таблиця 5.1 Перелік певних функцій для роботи із рядками символів

Ім’я функції Виконувана дія


strcpy(s1,s2) Копіювання s2 в s1
strcat(s1,s2) Конкатенація (приєднання) s2 в кінець s1
strlen(s1) Повертає довжину рядка s1
Повертає 0, якщо s1 та s2 співпадають, від’мне значення,
strcmp(s1,s2)
якщо s1<s2 та додатнє значення, якщо s1>s2
Повертає покажчик на перше входження символа ch в рядок
strchr(s1,ch)
s1
strstr(s1,s2) Повертає покажчик на перше входження рядка s2 в рядок s1
Ці функції оголошені в заголовковому файлі <string.h>. Застосування
бібліотечних функцій обробки рядків ілюструється наступним прикладом:

#include <stdio.h>
#include <string.h>
int main(void) {
char s1[80], s2[80];
gets(s1);
gets(s2);
printf("Довжина: %d %d\n", strlen(s1), strlen(s2));
if (!strcmp(s1, s2))
printf("Рядки рівні\n");
strcat(s1, s2);
printf("%s\n", s1);
strcpy(s1, "Перевірка.\n");
printf(s1);
if (strchr("Алло", 'л'))
printf(" л є в Алло\n");
if (strstr("Привіт", "ив"))
printf(" знайдено ив ");
return 0;
}

Якщо цю програму виконати і ввести в s1 і в s2 один і той же рядок "Алло!", то на


екран буде виведено наступне:

Довжина: 5 5
Рядки рівні
Алло!Алло!
Перевірка,
л є в Алло
знайдено ив
- 63 -

5.3. Двовимірні масиви


Стандартом Сі визначені багатовимірні масиви. Багатовимірні масиви можна
уявити собі у наступному вигляді:

Рис. 5.1. Приклади одно- дво- та тривимірного масивів

Найпростіша форма багатовимірного масиву - двовимірний масив. Двовимірний


масив - це масив, елементами якого є одновимірні масив.

Рис. 5.2. Двовимірний масив з розмірами 5х12

Оголошення двовимірного масиву d з розмірами 5 на 12 (малюнок вище)


виглядає наступним чином:

int d [5] [12];

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


одного комами. У мові Сі кожен вимір береться в окремі квадратні дужки. Аналогічно
зверненню до елемента одновимірного масиву, звернення до елемента з індексами
1 і 2 двовимірного масиву d виглядає так:

d[1][2]

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


від 1 до 12 і значення елементів виводяться на екран послідовно:

#include <stdio.h>
int main(void) {
- 64 -
int j, i, num[3][4];
for (j=0; j<3; ++j)
for (i=0; i<4; ++i)
num[j][i] = (j*4)+i+1;
/* вивід на екран */
for (j=0; j<3; ++j) {
for (i=0; i<4; ++i)
printf("%3d ", num[j][i]);
printf("\n");
}
return 0;
}

У цьому прикладі num[0][0] має значення 1, num[0][1] - значення 2, num[0][2] -


значення 3 і так далі. Наочно двомірний масив num із прикладу вище можна уявити
собі так:

num
|0 1 2 3
--+-----------
0|1 2 3 4
1|5 6 7 8
2 | 9 10 11 12

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


Перший індекс вказує номер рядка, а другий - номер стовпця. Для мови Сі
двовимірний масив у пам'яті розгортається по рядках.

Рис. 5.3. Розгортка двовимірного масиву на лінійну пам'ять у мові Сі.

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


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

кількість_байтів =
= розмір_1-го_виміру × розмір_2-го_виміру × sizeof(базовий_тип_елемента)

Наприклад, двовимірний масив 4-байтових цілих чисел розмірністю 10 × 5 займає


ділянку пам'яті розміром 10 × 5 × 4, тобто 200 байтів.
- 65 -

Масиви рядків

У програмах на мові Сі часто використовуються масиви рядків. Наприклад,


сервер бази даних звіряє команди користувачів з масивом допустимих команд. У
якості масиву рядків в мові Сі служить двовимірний символьний масив. Розмір
лівого вимірювання визначає кількість рядків, а правого - максимальну довжину
кожного рядка.
Наприклад, наступним оператором оголошується масив з 4 рядків з
максимальною довжиною кожного рядка 21 символ:

char str_array[4][21];

А от як зміст цього масива може виглядати в пам'яті комп'ютера:

Щоб звернутися до окремого рядка масиву, потрібно вказати тільки лівий індекс.
Наприклад, виклик функції gets() з третім рядком масиву str_array як аргументом
можна записати так:

gets (str_array[2]);

Цей оператор еквівалентний наступному:

gets (&str_array[2][0]);

З цих двох форм запису зручнішою є перша.

5.4. Багатовимірні масиви


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

тип ім’я_масиву [Розмір1] [Розмір2] ... [РозмірN];

Масиви, у яких число вимірів більше трьох, використовуються досить рідко, тому
що вони займають великий обсяг пам'яті. Наприклад, чотиривимірний масив
символів розмірністю 10x6x9x4 займає 2160 байтів. Якби масив містив
чотирибайтові цілі типу int, треба було б вже 8640 байтів. І обсяг необхідної пам'яті
з ростом числа вимірювань зростає експоненціально.
При зверненні до багатовимірних масивів комп'ютер багато часу витрачає на
обчислення адреси, так як при цьому доводиться враховувати значення кожного
індексу. Тому доступ до елементів багатовимірного масиву відбувається значно
повільніше, ніж до елементів одновимірного.
5.5. Ініціалізація масивів
У мові Сі масиви при оголошенні можна ініціалізувати. Загальна форма
ініціалізації масиву аналогічна ініціалізації змінної:
- 66 -

тип ім’я_масиву [розмір1] ... [розмірN] = {список_значень};

Список_значень є список констант, розділених комами. Типи констант повинні бути


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

5.4. Оголошення лінійного масиву з одночасною його ініціалізацією

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


ініціалізацію лінійного масиву:

5.5. Різновиди ініціалізації лінійного масиву

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


константами:

char ім’я_масиву [розмір] = "рядок";

У наступному прикладі масив str ініціалізується фразою "Мова Сі":

char str[8] = "Мова Сі";

Це оголошення можна записати і таким чином:

char str[8] = {‘М’, ‘о’, ‘в’, ‘а’, ’ ‘, 'C', 'і', '\0'};

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


необхідно ставити розмір масиву, достатній для того, щоб цей символ розмістився
у ньому. У попередньому прикладі розмір рядка заданий 8, хоча у фразі "Мова Сі"
міститься лише 7 символів. Якщо рядок ініціалізується рядковою константою,
компілятор автоматично додає нульовий символ в кінець рядка.
Багатовимірні масиви ініціалізуються так само, як і одновимірні. У наступному
прикладі масив sqrs ініціалізується числами від 1 до 10 і їх квадратами:
int sqrs[10][2] = {
1, 1,
2, 4,
3, 9,
- 67 -
4, 16,
5, 25,
6, 36,
7, 49,
8, 64,
9, 81,
10, 100
};

Ініціалізуючи багатовимірний масив, для поліпшення наочності елементи


ініціалізації кожного вимірювання можна брати у фігурні дужки. Цей спосіб
називається групуванням підагрегатів. З використанням цього прийому
попередній приклад може бути записаний так:

int sqrs[10][2] = {
{1, 1},
{2, 4},
{3, 9},
{4, 16},
{5, 25},
{6, 36},
{7, 49},
{8, 64},
{9, 81},
{10, 100}
};

Ще один наочний приклад, що дає уявлення про використання підагрегатів


при ініціалізації двовимірного масиву:

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

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


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

Рис. 5.7. Автоматична ініціалізація нулями елементів двовимірного масиву, які не


вказані явно.
- 68 -

Ініціалізація безрозмірних масивів

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


використовуючи ініціалізацію масивів:

char e1[18] = "Не можу прочитати \n";


char e2[17] = "Не можу записати \n";
char e3[22] = "Не можу відкрити файл \n";

Тут для задання розміру масиву довелося вручну підраховувати кількість символів
в кожному повідомленні. Однак в мові Сі є конструкція, завдяки якій компілятор
автоматично визначає необхідну довжину рядка. Якщо в операторі ініціалізації
масиву явно не вказано розмір масиву, компілятор створює масив такого розміру,
що в ньому вміщаються всі проініціалізовані елементи. Таким чином створюється
безрозмірний масив. Використовуючи цей метод, попередній приклад можна
записати так:

char e1[] = "Не можу прочитати \n";


char e2[] = "Не можу записати \n";
char e3[] = "Не можу відкрити файл \n";

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


дозволяє змінювати довжину будь-якого повідомлення, не піклуючись про
дотримання меж масивів.
Ініціалізація безрозмірних масивів підтримується не тільки для одновимірних
масивів. У багатовимірному масиві розмір самого лівого виміру також можна не
вказувати. (Розміри інших вимірів обов'язково повинні бути вказані, так як це
потрібно компілятору для визначення довжини підмасивів, складових масивів).
Таким чином можна створювати таблиці змінного розміру, компілятор автоматично
виділить необхіду для них пам'ять. Наприклад, оголошення sqrs як безрозмірного
масиву виглядає так:

int sqrs[][2] = {
{1, 1},
{2, 4},
{3, 9},
{4, 16},
{5, 25},
{6, 36},
{7, 49},
{8, 64},
{9, 81},
{10, 100}
};

Перевага безрозмірного оголошення масиву полягає в тому, що можна змінювати


довжину таблиці, не піклуючись про розмір масиву.
- 69 -

ТЕМА 6. ВИКОРИСТАННЯ ПОКАЖЧИКІВ

Покажчик - це змінна, значенням якої є адреса деякого об'єкта (зазвичай іншої


змінної) в пам'яті комп'ютера. Наприклад, якщо одна змінна містить адресу іншої
змінної, то говорять, що перша змінна показує (посилається) на другу. Це
ілюструється за допомогою наступного малюнка
Адреса Значення
комірки змінної в
пам’яті пам’яті
+----------+
1000 | 1004 |--.
+----------+ |
1001 | | |
+----------+ |
1002 | | |
+----------+ |
1003 | | |
+----------+ |
1004 | |<-'
+----------+
1005 | |
+----------+
1006 | |
+----------+

Рис. 6.1. Покажчики в пам’яті комп’ютера

Покажчик широко використовується програмістами з двох причин:


 замість того, щоб повністю копіювати дані можна просто передати на них
покажчик;

 може виникнути ситуація, коли дві ділянки коду повинні працювати з одним і тим
самим фрагментом даних, а не з його окремими копіями.
- 70 -

6.1. Оголошення покажчиків та операції для роботи з ними


Змінну, що є покажчиком, потрібно відповідним чином оголосити. Оголошення
покажчика складається з імені базового типу, символу * і імені змінної. Загальна
форма оголошення покажчика наступна:

тип *ім’я;

Тут тип - це базовий тип покажчика, ним може бути будь-який правильний тип. Ім'я
визначає ім'я змінної-покажчика.
Базовий тип покажчика визначає тип об'єкта, на який покажчик буде посилатися.
Фактично покажчик будь-якого типу може посилатися на будь-яке місце в пам'яті.
Однак виконувані з покажчиком операції істотно залежать від його типу. Наприклад,
якщо оголошено покажчик типу int*, компілятор припускає, що будь-яка адреса, на
яку він посилається, містить змінну типу int, хоч це може бути і не так. Отже,
оголошуючи покажчик, необхідно переконатися, що його тип сумісний з типом
об'єкта, на який він буде посилатися.
У мові Сі визначені дві операції для роботи з покажчиками: * і &. Оператор & -
це унарний оператор, який повертає адресу свого операнда. (Нагадаємо, що
унарний оператор має один операнд). Наприклад, оператор

m = &count;

присвоює змінній m адресу змінної count. Адреса - це номер першого байта ділянки
пам'яті, в якому зберігається змінна. Адреса і значення змінної - це абсолютно різні
поняття. Оператор & можна уявити собі як оператор, який повертає адресу об'єкта.
Отже, попередній приклад можна прочитати так: "змінній m присвоюється адреса
змінної count".
Припустимо, змінна count зберігається в комірці пам'яті під номером 2000, а
її значення дорівнює 100. Тоді змінній m буде присвоєно значення 2000.
Друга операція для роботи з покажчиками - операція розіменування
покажчика (її знак, тобто оператор, *) виконує дію, зворотну по відношенню до &.
Оператор * - це унарний оператор, який повертає значення змінної, розташованої
за вказаною адресою. Наприклад, якщо m містить адресу змінної count, то оператор

q = *m;

присвоює змінній q значення змінної count. Таким чином, q отримає значення 100,
тому що за адресою 2000 розташована змінна count, яка має значення 100. Дію
оператора * можна виразити словами "значення за адресою", тоді попередній
оператор може бути прочитаний так: "q отримує значення змінної, розташованої за
адресою m".

6.2. Використання покажчиків у виразах


У загальному випадку вирази з покажчиками підкоряються тим же правилам, що
і звичайні вирази. Розглянемо застосування виразів з покажчиками в операціях
присвоєння, перетворення типів, а також в операціях "покажчикової" арифметики.

Присвоювання покажчиків

Покажчик можна використовувати в правій частині оператора присвоювання для


присвоювання його значення іншому покажчику. Якщо обидва покажчики мають
- 71 -
один і той самий тип, то виконується просте присвоювання, без перетворення типу.
У наступному прикладі

#include <stdio.h>
int main (void) {
int x = 99;
int *p1, *p2;
p1 = &x;
p2 = p1;
/* Друк значення x двічі */
printf ( "Значення за адресою p1 і p2:%d%d \ n", *p1, *p2);
/* Друк адреси x двічі */
printf ( "Значення покажчиків p1 і p2:%p %p", p1, p2);
return 0;
}

після присвоювання

p1 = & x;
p2 = p1;

обидва покажчика (p1 і р2) посилаються на х. Тобто, обидва покажчики посилаються


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

Значення за адресою p1 і р2: 99 99


Значення покажчиків p1 і р2: 0063FDF0 0063FDF0

Для виведення значень покажчиків у функції printf() використовується


специфікатор формату %р, який виводить адреси в форматі, використовуваному
компілятором.
Допускається присвоювання покажчика одного типу покажчику іншого типу.
Однак для цього необхідно виконати явне перетворення типу покажчика (операція
приведення типів), яка розглядається в наступному підпункті.

Перетворення типу покажчика

Покажчик можна перетворити в інший тип. Ці перетворення бувають двох видів:


з використанням покажчика типу void* і без його використання.
У мові Сі допускається присвоювання покажчика типу void* покажчику будь-
якого іншого типу (і навпаки) без явного перетворення типу покажчика. Тип
покажчика void* використовується, якщо тип об'єкта невідомий. Наприклад,
використання типу void* в якості параметра функції дозволяє передавати в функцію
покажчик на об'єкт будь-якого типу, при цьому повідомлення про помилку не
генерується. Також такий тип корисний для посилання на довільну комірку пам'яті,
незалежно від типу розміщених там об'єктів. Наприклад, функція розміщення
malloc() (розглядається далі в цій темі) повертає значення типу void*, що дозволяє
використовувати її для розміщення в пам'яті об'єктів будь-якого типу.
На відміну від void*, перетворення всіх інших типів вказівників повинні бути
завжди явними (тобто повинна бути вказана операція приведення типів). Однак слід
враховувати, що перетворення одного типу покажчика в інший може викликати
непередбачувану поведінку програми. Наприклад, в наступному прикладі
здійснюється спроба присвоїти значення х змінній у за допомогою покажчика р. При
- 72 -
компіляції програми повідомлення про помилку не генерується, однак результат
роботи програми буде невірний.

#include <stdio.h>
int main (void) {
float x = 100.1, y;
short int *p;
/* У наступному операторі покажчику на ціле p
присвоюється значення, що посилається на double. */
p = (short int*) &x;

/* Наступний оператор працює не так, як очікується. * /


y = *p; / * Спроба присвоєння y значення x за допомогою покажчика p */

/* Наступний оператор не виведе число 100.1. */


printf ("Значення x дорівнює:%f (Це не так!)", y);

return 0;
}

Операція перетворення типів застосовується в операторі присвоєння адреси


змінної х (вона має тип float*) покажчику p, тип якого short int*. Перетворення типу
виконано коректно, проте програма буде працювати не так, як очікується. Для
роз'яснення проблеми згадаємо, що змінна short int займає в пам'яті 2 байта, а float
- 4 байта. У пам'яті змінна x типу float буде виглядати у шістьнадцятирічному і
двійковому вигляді, як (значення бітів умовні, не рахувалися!)

1 01111101 011000000000000000000002 = BEB0000016

Покажчик p оголошений, як покажчик на ціле півслова (тобто типу short int),


тому оператор присвоювання

y = *р;

передасть змінній y тільки молодші 2 байта інформації (виділено у прикладі


червоним кольором), а не всі 4 байтів, необхідних для float. Незважаючи на те, що
p посилається на об'єкт float, оператор присвоювання виконає дію з об'єктом типу
short int, тому що p оголошений як покажчик на short int. Тому таке використання
покажчика p неправильне.
Наведений приклад підтверджує те, що операції з покажчиками виконуються
в залежності від базового типу покажчиків. Синтаксично допускається посилання на
об'єкт з типом, відмінним від типу покажчика, однак при цьому покажчик буде
"думати", що він посилається на об'єкт свого типу. Таким чином, операції з
покажчиками керуються типом покажчика, а не типом об'єкта, на який він
посилається.
Дозволено ще один тип перетворень: перетворення цілого в покажчик та
навпаки. В цьому випадку необхідно застосувати операцію явного перетворення
типів. Однак користуватися цим засобом потрібно дуже обережно, тому що при
цьому легко отримати непередбачувану поведінку програми. Явне перетворення
типу необов'язкове, якщо покажчику присвоюється значення нуля, тобто нульовий
покажчик NULL.
- 73 -

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

У мові Сі допустимі тільки дві арифметичні операції над покажчиками:


додавання і віднімання. Припустимо, поточне значення покажчика p1 типу int*
дорівнює 2000. Згадаємо також, що змінна типу int займає в пам'яті 4 байта. Тоді
після операції збільшення

p1++;

покажчик p1 прийме значення 2004, а НЕ 2001. Тобто, при збільшенні на 1 покажчик


p1 буде посилатися на наступне ціле число. Це ж справедливо і для операції
зменшення.
Операції адресної арифметики підкоряються наступним правилам. Після
виконання операції збільшення над покажчиком, даний покажчик буде посилатися
на наступний об'єкт свого базового типу. Після виконання операції зменшення - на
попередній об'єкт. Стосовно покажчиків на char, операції адресної арифметики
виконуються як звичайні арифметичні операції, тому що довжина об'єкта char
завжди дорівнює 1. Для всіх покажчиків адреса збільшується або зменшується на
величину, яка дорівнює розміру об'єкта того типу, на який вони вказують. Тому
покажчик завжди посилається на об'єкт з типом, тотожним базовому типу
покажчика. Ця концепція ілюструється за допомогою наступного малюнка:

char *ch = (char *) 3000;


short int *i = (short int *) 3000;

+------+
ch --->| 3000 |<--- i.
+------+
ch+1 ->| 3001 |
+------+
ch+2 ->| 3002 |<--- i+1.
+------+
ch+3 ->| 3003 |
+------+
ch+4 ->| 3004 |<--- i+2.
+------+
ch+5 ->| 3005 |
+------+
Рис. 6.2. Як працює арифметика покажчиків

Операції адресної арифметики не обмежені збільшенням (інкрементом) і


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

p1 = p1 + 12;

"пересуває" покажчик p1 на 12 об'єктів в бік збільшення адрес.

Крім додавання і віднімання покажчика і цілого, дозволена ще тільки одна


операція адресної арифметики: можна віднімати два покажчика один від другого.
Завдяки цьому можна визначити кількість об'єктів, розташованих між адресами, на
які вказують дані два покажчика; правда, при цьому вважається, що тип об'єктів
збігається з базовим типом покажчиків. Всі інші арифметичні операції заборонені. А
саме: не можна ділити і множити покажчики, підсумувати два покажчика, виконувати
- 74 -
над покажчиками побітові операції, підсумовувати покажчик зі значеннями, що
мають тип float або double і т.д.

Порівняння покажчиків

Стандартом Сі допускається порівняння двох покажчиків. Наприклад, якщо


оголошені два покажчика р і q, то наступний оператор є правильним:

if (p <q) printf ( "p посилається на меншу адресу, ніж q \ n");

Як правило, порівняння покажчиків може виявитися корисним тільки тоді, коли


обидва покажчика посилаються на загальний об'єкт, наприклад, на масив. Як
приклад розглянемо програму з двома стековими функціями, призначеними для
запису і зчитування цілих чисел в/із стеку.
Стек - це список, який використовує систему доступу "першим прийшов -
останнім пішов". Іноді стек порівнюють зі стопкою тарілок на столі: перша,
поставлена на стіл, буде взята останньої. Стеки часто використовуються в
компіляторах, інтерпретаторах, програмах обробки великоформатних таблиць і в
інших системних програмах. Для створення стека необхідні дві функції: push() і
pop(). Функція push() заносить числа в стек, a pop() - витягує їх звідти. В даному
прикладі ці функції використовуються у програмі main(). При введенні числа з
клавіатури, програма розміщує його в стеку. Якщо ввести 0, то число вилучають зі
стека. Програма завершує роботу при введенні -1.

#include<stdio.h>
#include<stdlib.h>
#define SIZE 5

void push(int i);

int pop(void);

int *tos, *p1, stack[SIZE];

int main(void)

{
int value;

tos = stack;
p1 = stack;

do {
printf("Введіть число: ");
scanf_s("%d", &value);

if (value != 0)
push(value);
else
printf("Значення на вершині рівне: %d\n", pop());

} while (value != -1);

return 0;
}

void push(int i)
{
p1++;
- 75 -
if (p1 > (tos + SIZE)) {
printf("Стек переповнений.\n");
exit(1);
}
*p1 = i;
return;
}

int pop(void)
{
if (p1 == tos) {
printf("Стек порожній.\n");
exit(1);
}
p1--;
return *(p1 + 1);
}

Стек зберігається в масиві stack. Спочатку покажчики p1 і tos встановлюються на


перший елемент масиву stack. Надалі p1 посилається на верхній елемент стека, a
tos продовжує зберігати адресу початку стека. Після ініціалізації стека
використовуються функції push() і pop(). Вони виконують запис в стек і зчитування
з нього, перевіряючи кожен раз дотримання границь стека. У функції push()
перевіряється, чи покажчик p1 не перевищує верхньої границі стека tos + SIZE. Це
запобігає переповненню стека. У функції pop() перевіряється, чи покажчик p1 не
виходить за нижню границю стека.
В операторі return функції pop() дужки необхідні тому, що без них оператор

return *p1 + 1;

повернув би значення, розташоване за адресою p1, збільшене на 1, а не значення


за адресою p1 + 1.

6.3. Масиви та покажчики


Покажчики та масиви тісно пов'язані один з одним. Ім'я масиву без індексу - це
покажчик на перший (початковий) елемент масиву. Розглянемо, наприклад,
наступний масив:

char p[10];

Наступні два вирази ідентичні:

p
&p[0]

вираз

p == &p[0]

приймає значення ІСТИНА, тому що адреса 1-го елемента масиву - це те ж саме,


що і адреса масиву.
Як уже зазначалося, ім'я масиву без індексу є покажчиком. І навпаки,
покажчик можна індексувати як масив. Розглянемо наступний фрагмент програми:

int *p, i[10];


- 76 -
p = i;
p[5] = 100; / * У присвоєнні використовується індекс * /
*(p + 5) = 100; / * У присвоєнні використовується адресна арифметика * /

Обидва оператори присвоювання заносять число 100 в 6-й елемент масиву i.


Перший з них індексує покажчик p, у другому застосовуються правила адресної
арифметики. В обох випадках виходить один і той самий результат.
Можна також індексувати покажчики на багатовимірні масиви. Наприклад,
якщо а - це покажчик на двовимірний масив цілих розмірністю 10 × 10, то наступні
два вирази еквівалентні:

a
&a[0][0]

Більш того, до елементу (0,4) можна звернутися двома способами: або вказавши
індекси масиву: а[0][4], або за допомогою покажчика: *((int*) а + 4). Аналогічно для
елемента (1,2): а[1][2] або *((int*) а + 12). У загальному вигляді для двовимірного
масиву справедлива наступна формула:

a[j][k] = *((базовий_тип*)а+(j*довжина_рядка)+k)

Правила адресної арифметики вимагають явного перетворення покажчика на


масив в покажчик на базовий тип. Покажчики використовуються для звернення до
елементів масиву тому, що часто операції адресної арифметики виконуються
швидше, ніж індексація масиву.
Двовимірний масив може бути представлений як покажчик на масив
одновимірних масивів. Додавши ще один покажчик, можна з його допомогою
звертатися до елементів окремого рядка масиву. Цей прийом демонструється в
функції pr_row(), яка друкує вміст конкретнго рядка двовимірного глобального
масиву num:

Приклад 6.3

void pr_row(int i, int num[][SIZE]) {


int *p;

p = (int*) &num[i]; /* Обчислення адреси 1-го


елемента рядка номер i */
for (int j = 0; j < SIZE; ++j)
printf("%d ", *(p + j));
printf("\n");
return;
}

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


для будь-яких багатовимірних. Наприклад, замість того, щоб працювати з
тривимірним масивом, можна використовувати покажчик на двовимірний масив,
причому замість нього в свою чергу можна використовувати покажчик на
одновимірний масив. У загальному випадку замість того, щоб звертатися до n -
вимірного масиву, можна працювати з покажчиком на (n-1) - вимірний масив.
Причому цей процес зниження вимірів закінчується на одновимірному масиві.
- 77 -

6.4. Масиви покажчиків


Як і об'єкти будь-яких інших типів, покажчики можуть бути зібрані в масив. У
наступному операторі оголошений масив з 10 покажчиків на об'єкти типу int:

int* x[10];

Для присвоєння, наприклад, адреси змінної var третьому елементу масиву


покажчиків, необхідно написати:

x[2] = &var;

В результаті цієї операції, такий вираз *х[2] має те саме значення, що і змінна var:
Масиви покажчиків часто використовуються при роботі з рядками. Наприклад,
можна написати функцію, що виводить потрібний рядок (розміщенний за індексом
num) з повідомленням про помилку:

Приклад 6.4

void syntax_error (int num) {


static char* err[] = {
"Не можна відкрити файл \n",
"Помилка при читанні \n",
"Помилка при запису \n",
"Неякісний носій \n"
};

printf ( "%s", err[num]);


}

Масив err містить покажчики на рядки з повідомленнями про помилки. Тут рядкові
константи у виразі ініціалізації створюють покажчики на рядки. Аргументом функції
printf() служить один з покажчиків масиву err, який відповідно до індексу num вказує
на потрібний рядок з повідомленням про помилку. Наприклад, якщо в функцію
syntax_error() передається значення 2, то виводиться повідомлення "Помилка при
запису".
6.5. Багаторівнева адресація
Іноді покажчик може посилатися на покажчик, який вже сам посилається на
значення змінної. Це називається багаторівневою адресацією. Малюнок нижче
ілюструє концепцію багаторівневої адресації.

Покажчик Змінна
+--------+ +--------+
| Адреса |------->|Значення|
- 78 -
+--------+ +--------+

Однорівнева адресація

Покажчик Покажчик Змінна


+--------+ +--------+ +--------+
| Адреса |----->| Адреса |----->|Значення|
+--------+ +--------+ +--------+

Багаторівнева адресація

Рис. 6.3. Як працює багаторівнева адресація

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


який і містить потрібне значення. У випадку дворівневої адресації перший покажчик
містить адресу другого покажчика, який вже і містить адресу об'єкта з потрібним
значенням.
Багаторівнева адресація може мати скільки завгодно рівнів, однак рівні, глибші
другого, тобто покажчики глибші, ніж "покажчики на покажчики" застосовуються
вкрай рідко. Справа в тому, що при використанні таких покажчиків часто
зустрічаються концептуальні помилки через те, що сенс таких покажчиків
людському мозку уявити важко.
Змінна, що є покажчиком на покажчик, повинна бути відповідним чином
оголошена. Це робиться за допомогою двох зірочок перед ім'ям змінної. Наприклад,
в наступному операторі змінна newbalance оголошена як покажчик на покажчик на
змінну типу float:

float** newbalance;

Слід добре розуміти, що newbalance - це не покажчик на число типу float, а покажчик


на покажчик на число типу float.
При дворівневій адресації для доступу до значення об'єкта потрібно
поставити перед ідентифікатором дві зірочки:

Приклад 6.5

#include <stdio.h>
int main (void) {
int x, *p, **q;
x = 10;
p = &x;
q = &p;
printf ( "%d", **q); /* Друк значення x */
return 0;
}

Тут p оголошена як покажчик на ціле, a q - як покажчик на покажчик на ціле. Функція


printf() виводить на екран число 10.
Слід зауважити, що неправильне застосування багаторівневих покажчиків
істотно ускладнює програму, робить її незручною для читання і схильною до
помилок. Тому використовуються вони виключно для організації роботи з дво- та
тривимірними масивами.
- 79 -

6.6. Ініціалізація покажчиків


Після оголошення локального покажчика до першого присвоєння він містить
невизначене значення. Глобальні і статичні локальні покажчики при оголошенні
завжди неявно ініціалізуються нулем! Якщо спробувати використовувати локальний
покажчик перед присвоєнням йому потрібного значення, то швидше за все він
миттєво зруйнує програму або всю операційну систему.
При роботі з покажчиками більшість програмістів дотримуються наступної
важливої угоди: покажчик, що не посилається в поточний момент часу належним
чином на конкретний об'єкт, повинен містити нульове значення. Нуль
використовується тому, що Сі гарантує відсутність чого-небудь за нульовою
адресою. Отже, якщо покажчик дорівнює нулю, то це означає, по-перше, що він ні
на що не посилається, а по-друге - що його зараз не можна використовувати.
Покажчику можна задати нульове значення, присвоївши йому 0. Наприклад,
наступний оператор ініціалізує покажчик р нулем:

char *p = 0;

В багатьох файлах заголовків мови Сі, наприклад, в <stdio.h> визначено


макрос NULL, який є нульовою покажчиковою константою. Тому в програмах на Сі
часто можна побачити наступне присвоювання:

p = NULL;

Однак рівність покажчика нулю не робить його абсолютно "безпечним".


Використання нуля як ознаки непідготовленості покажчика - це тільки угода
програмістів, але не правило мови Сі. У наступному прикладі компіляція пройде без
помилки, а результат, тим не менш, буде неправильним:

int *p = 0;
*р = 10; /* Помилка! */

В цьому випадку спроба виконання присвоювання за допомогою покажчика p буде


присвоюванням за нульовою адресою, що зазвичай також викликає аварійне
завершення програми.
У багатьох функціях для підвищення ефективності програми можна
використовувати те, що нульовий покажчик свідомо вважається непідготовленим
для використання. Наприклад, можна використовувати нульовий покажчик як ознаку
кінця масиву покажчиків (за аналогією з нульовим термінальним символом рядка).
Функція, яка використовує масив покажчиків, таким чином дізнається про кінець
масиву без додаткового оголошення його розміру. Такий підхід ілюструється в
наступному прикладі. Переглядаючи список імен, функція search() визначає, чи є в
цьому списку задане ім'я.

Приклад 6.6

#include <stdio.h>
#include <string.h>

int search (char* p[], char* name);

char* names[] = {
"Сергій",
- 80 -
"Юрій",
"Ольга",
"Ігор",
NULL}; /* Нульова константа завершує список */

int main(void) {
char name[78] = { 0 };

while (strcmp(name,"0") != 0) {
printf("Введіть чиєсь ім'я, або 0 для закінчення:");
gets_s(name);
if (search(names, name) != -1)
printf("Ім'я %s є в списку \n", name);
else
printf("Імені %s в списку не знайдено \n", name);
}

return 0;
}

/* Перегляд імен. */
int search(char* p[], char* name) {
register int i;

for (i = 0; p[i]; ++i)


if (!strcmp(p[i], name))
return i;
return -1; /* Ім'я не знайдено */
}

Тут у функцію search() передаються два параметри. Перший з них, p - масив


покажчиків на рядки, що являють собою імена зі списку. Другий параметр name - є
покажчиком на рядок із заданим ім'ям. Функція search() переглядає масив
покажчиків, поки не знайде рядок, що збігається з рядком, на який вказує name.
Ітерації циклу for повторюються до тих пір, поки не відбудеться збіг імен, або не
зустрінеться нульовий покажчик. Кінець масиву відзначений нульовим покажчиком,
тому при досягненні кінця масиву керуюча умова циклу прийме значення ХИБНЕ.
Іншими словами, p[t] має значення ХИБНЕ, коли p[t] є нульовим покажчиком. У
розглянутому прикладі саме це і відбувається, коли йде пошук імені "Павло", якого
в списку немає.
У програмах на Сі покажчик типу char* часто ініціалізують строковою
константою (як в попередньому прикладі). Розглянемо наступний приклад:

char* p = "тестовий рядок";

Змінна р є покажчиком, а не масивом. Тому виникає логічне запитання: де


зберігається строкова константа "тестовий рядок"? Так як p не є масивом, вона не
може зберігатися в p, тим не менш, вона десь записана. Щоб відповісти на це
питання, потрібно знати, що відбувається, коли компілятор зустрічає строкову
константу. Компілятор створює так звану таблицю рядків, в ній він і зберігає рядкові
константи, які зустрічаються йому по ходу зчитування тексту програми. Отже, коли
зустрічається оголошення з ініціалізацією, компілятор зберігає рядок "тестовий
- 81 -
рядок" в таблиці рядків, а в покажчик p записує його адресу. Далі в програмі
покажчик p може бути використаний вже у якості рядка.

6.7. Функції динамічного розподілу пам'яті


Покажчики використовуються для динамічного розподілу оперативної пам'яті
комп'ютера при зберігання даних програмою. Динамічний розподіл означає, що
програма виділяє пам'ять для даних під час свого виконання по мірі потреби. В
зв'язку з цим нагадаємо, що пам'ять для глобальних змінних виділяється на початку
роботи програми, а для не статичних локальних змінних - в стеку програми при
виклику функції. Під час виконання програми ні глобальним, ні локальним змінним
не може бути виділена додаткова пам'ять. Але досить часто така необхідність
виникає, причому обсяг необхідної пам'яті заздалегідь невідомий. Таке трапляється,
наприклад, при використанні динамічних структур даних, таких як зв'язні списки або
бінарні дерева. Такі структури даних при виконанні програми розширюються або
скорочуються в міру необхідності. Для реалізації таких структур програмі потрібні
ресурси, здатні по мірі необхідності виділяти і звільняти для них пам'ять.
Пам'ять, що виділяється в Сі функціями динамічного розподілу даних,
знаходиться в так званній пам'яті, що динамічно розподіляється (Купа, Heap).
Динамічно розподілена область пам'яті - це додатково виділена програмі область
пам'яті, що не використовується змінними програми, операційною системою або
іншими програмами. Розмір динамічно розподіленої області пам'яті для програми
обумовлюється параметрами компіляції і вибирається самим програмістом з тих
міркувань, що в ній повинні розміститися всі необхідні динамічні структури програми.
Більшість компіляторів підтримують бібліотечні функції, що дозволяють отримати
поточний розмір максимального неперервного блоку ще вільної динамічно
розподіленої області пам'яті, проте ці функції не визначені в Стандарті С89. Хоча
розмір динамічно розподіленої області пам'яті може бути дуже великий, все ж вона
скінченна і може бути вичерпана, тому можливість створення нових динамічних
структур повинна завжди перевірятися програмістом перед спробою їх першого
використання.
Основу системи динамічного розподілу пам'яті в Сі складають функції malloc() і
free(). Ці функції працюють спільно. Функція malloc() виділяє пам'ять, а free() -
звільняє її. Це означає, що при кожному запиті функція malloc() виділяє неперервну
область пам'яті необхідного розміру з наявних, a free() звільняє її, тобто повертає
системі. В програму, яка використовує ці функції, потрібно включити заголовковий
файл <stdlib.h>.
Прототип функції malloc() наступний:

void* malloc (size_t кількість_байтів);

Тут кількість_байтів – це розмір пам'яті, необхідної для розміщення даних. (Тип


size_t теж визначено в <stdlib.h> як unsigned int - ціле число без знака.) Функція
malloc() повертає покажчик типу void*, тому його можна присвоїти покажчику будь-
якого типу. При успішному виконанні malloc() повертає покажчик на перший байт
неперервної ділянки пам'яті, виділеної в динамічній області пам'яті. Якщо в
динамічній пам'яті недостатньо вільного місця для виконання запиту, то пам'ять не
виділяється і malloc() повертає нуль.
При виконанні наступного фрагмента програми виділяється неперервна ділянка
пам'яті об'ємом 1000 байтів:

char *p;
p = (char *) malloc (1000); / * Виділення 1000 байтів * /
- 82 -

Після присвоєння покажчик p посилається на перший з 1000 байтів виділеної


ділянки пам'яті.
У наступному прикладі виділяється пам'ять для 50 цілих чисел. Для підвищення
мобільності (переносу програми з однієї машини на іншу) використовується
операція sizeof.

int *p;
p = (int*) malloc (50 * sizeof (int));

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


необхідно перевіряти, чи воно відбулося. Якщо malloc() не змогла з якої-небудь
причини виділити необхідну ділянку пам'яті, то вона повертає нуль. У наступному
прикладі показано, як виконується перевірка успішності розміщення:

p = (int*) malloc (200);


if (!p) {
printf ( "Брак пам'яті. \ n");
exit (1);
}

Замість виходу з програми exit() можна поставити будь-який обробник помилки.


Обов'язковою тут можна назвати лише вимогу не використовувати покажчик р,
якщо він дорівнює нулю.
Функція free() по своїй дії протилежна функції malloc() в тому сенсі, що вона
повертає системі ділянку пам'яті, виділену раніше з допомогою функції malloc().
Іншими словами, вона звільняє ділянку пам'яті, яка може бути знову використана
функцією malloc() в подальшому. Функція free() має наступний прототип:

void free (void *p);

Тут р - покажчик на ділянку пам'яті, виділений перед цим функцією malloc(). Функцію
free() ні в якому разі не можна викликати з неправильним аргументом (тобто,
аргументом, для якого не було раніше виділено пам'ять функцією malloc()). Це
миттєво зруйнує всю систему розподілу пам'яті і приведе до аваріного завершення
програми.
Підсистема динамічного розподілу пам'яті в Сі використовується спільно з
покажчиками для створення різних структур даних, таких, наприклад як динамічні
масиви.
Стандартом мови С89 визначені ще дві додаткові функції розподілу пам'яті,
що полегшують роботу з динамічними структурами. Перша з них - calloc().

void* calloc (size_t num, size_t size);

Функція calloc() виділяє пам'ять, розмір якої дорівнює добутку num * size, тобто
пам'ять, достатню для розміщення масиву, що містить num об'єктів розміром size.
Всі байти виділеної пам'яті при цьому ініціалізуються нулями.
Функція calloc() повертає покажчик на перший байт виділеної області пам'яті.
Якщо для задоволення запиту немає достатнього обсягу неперервної пам'яті,
повертається нульовий покажчик. Перед спробою використовувати розподілену
пам'ять важливо перевірити, чи повернуте значення не дорівнює нулю. Слід
пам'ятати, що оскільки функція calloc() ініціалізує виділену область нулями -
- 83 -
продуктивність (швидкість роботи) її значно нижче, ніж у функції malloc(). Тому
використовувати її потрібно лише там, де вона дійсно потрібна.

Приклад 6.7

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


масиву з 100 чисел типу float:

#include <stdlib.h>
#include <stdio.h>
float* get_mem (void) {
float* p;
p = (float*) calloc (100, sizeof (float));
if (!p) {
printf ("Не вистачає пам'яті! \ n");
exit (1);
}
return p;
}

Ще одна функція мови Сі, призначена для роботи з динамічною пам'яттю,


називається realloc(). Використовується вона, якщо у програміста виникає
необхідність змінити розмір раніше виділеного блоку динамічної пам'яті.
Виглядає вона наступним чином:

void* realloc (void *ptr, size_t size);

Тут ptr являє собою покажчик на динамічну змінну, розмір якої необхідно змінити.
Параметр size являє собою новий розмір динамічного об'єкту в байтах.
У С89 функція realloc() змінює розмір блоку раніше виділеної пам'яті, що
адресується покажчиком ptr відповідно до заданого нового розміру size. Значення
параметра size може бути більше або менше поточного розміру області, що
адресується ptr. Функція realloc() повертає покажчик на блок пам'яті, оскільки не
виключена необхідність переміщення цього блоку (наприклад, при збільшенні
розміру блоку пам'яті). У цьому випадку вміст старого блоку динамічної змінної
повністю копіюється в новий блок.
Якщо покажчик ptr нульовий, функція realloc() просто виділяє size байтів
пам'яті і повертає покажчик на цю пам'ять (працює як функція malloc()). Якщо
значення параметра size дорівнює нулю, пам'ять, що адресується параметром ptr,
звільняється (працює як функція free()).
Якщо в динамічній області пам'яті немає достатнього обсягу вільної
неперевної пам'яті для виділення size байтів, повертається нульовий покажчик, а
вхідний блок пам'яті залишається незмінним.

Приклад 6.8

Ця програма спочатку виділяє блок пам'яті для 16 символів плюс '\0', копіює в
них рядок "Це - 16 символів", а потім використовує realloc() для збільшення розміру
блоку до 17 символів, щоб розмістити в кінці крапку.

#include <stdlib.h>
#include <stdio.h>
- 84 -
#include <string.h>

int main (void) {


char *p,
*pSave;

p = (char*) malloc (17);


if (!p) {
printf ("Не вистачає пам'яті! \ n");
exit (1);
}

strcpy (p, "Це - 16 символів");


pSave = p;
p = (char*) realloc (p, 18);
if (!p) {
printf ("Не вистачає пам'яті! \ n");
p = pSave;
exit (1);
}

strcat (p, ".");


printf ("%s", p);
free (p);
return 0;
}

6.8. Динамічне виділення пам’ятi для масивiв


Досить часто виникає необхідність виділити пам'ять динамічно, використовуючи
malloc(), але працювати з цією пам'яттю зручніше так, ніби це масив, який можна
індексувати. В цьому випадку потрібно створити динамічний масив. Зробити це
нескладно, тому що кожен покажчик можна індексувати як масив. У наступному
прикладі одновимірний динамічний масив містить рядок тексту:

Приклад 6.9

/ * Динамічний розподіл рядка, рядок вводиться


користувачем, а потім роздруковується справа наліво. * /
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main (void) {


char *s;
register int t;
s = (char*) malloc (81);
if (!s) {
printf ( " Не вистачає пам'яті! \ n");
exit (1);
}
gets (s);
- 85 -
for (t = strlen(s) -1; t >= 0; t--)
putchar (s[t]);
free (s);
return 0;
}

Перед першим використанням s програма перевіряє, чи успішно пройшло виділення


пам'яті. Ця перевірка необхідна для запобігання випадковому використанню
нульового покажчика. Покажчик s використовується в функції gets(), а також при
виведенні на екран (але на цей раз вже як звичайний масив).
Можна також динамічно виділити пам'ять для багатовимірного масиву. Для
цього потрібно оголосити покажчик, що визначає всі виміри, окрім самого лівого
виміру масиву. У наступному прикладі двомірний динамічний масив містить
таблицю чисел від 1 до 10 в степені 1, 2, 3 і 4.

Приклад 6.10

#include <stdio.h>
#include <stdlib.h>

#define SIZE 10

int pwr(int a, int b);

int main(void) {
/* Оголошення покажчика на масив з SIZE рядків
в яких зберігаються цілі числа(int). */
int (*p)[SIZE];
register int i, j;

SetConsoleCP(1251);
SetConsoleOutputCP(1251);

/*Виділення пам'яті для масиву 4 x SIZE */


p = (int(*)[SIZE]) malloc(4 * SIZE * sizeof(int));

if (!p) {
printf("Необхідна пам'ять не виділена. \n");
exit(1);
}

for (i = 0; i < SIZE; i++)


for (j = 0; j < 4; j++)
p[j][i] = pwr(i+1, j+1);

for (i = 0; i < SIZE; i++) {


for (j = 0; j < 4; j++)
printf("%10d", p[j][i]);
printf("\n");
}

free(p);
- 86 -
return 0;
}

/*Піднесення цілого числа a до степені b. */


int pwr(int a, int b) {
register int t = 1;

for (; b; b--)
t = t * a;
return t;
}

Програма виводить на екран наступне:

1 1 1 1
2 4 8 16
3 9 27 81
4 16 64 256
5 25 125 625
6 36 216 1296
7 49 343 2401
8 64 512 4096
9 81 729 6561
10 100 1000 10000

Покажчик р в головній програмі main() оголошений як

int (*p)[10];

Слід зазначити, що дужки навколо *р обов'язкові. Таке оголошення означає, що


р вказує на масив з 10 цілих чисел. Якщо збільшити покажчик р на 1, то він буде
вказувати на наступні 10 цілих чисел. Таким чином, р - це покажчик на двовимірний
масив з 10 числами в кожному стовпчику. Тому р можна індексувати як звичайний
двовимірний масив. Різниця тільки в тому, що тут пам'ять виділена за допомогою
malloc(), а для звичайного масиву пам'ять виділяється у стековій області на
початку роботи функції автоматично.
6.9. Помилки при роботі з покажчиками
Ніщо не може доставити більше неприємностей, як "дикий" покажчик! Покажчики
схожі на двогостру зброю: їх можливості величезні, проте виявити помилки в них
надзвичайно важко.
Помилковий покажчик важко знайти тому, що помилка в самому покажчику ніяк
себе не проявляє. Проблеми виникають при спробі звернутися до об'єкта за
допомогою цього покажчика. Якщо значення покажчика неправильне, то програма з
його допомогою звертається до довільної комірки пам'яті. При читанні в програму
потрапляють неправильні дані, а під час запису спотворюються інші дані, що
зберігаються в пам'яті, або псується ділянка програми, на яку вказує помилковий
покажчик. В обох випадках помилка може не виявитися зовсім або проявитися
пізніше в формі, що не вказує на її причину.
Оскільки помилки, пов'язані з покажчиками, особливо важко виявляти, при
роботі з покажчиками слід дотримуватися особливої обережності. Розглянемо деякі
- 87 -
помилки, що найчастіше виникають при роботі з покажчиками. Класичний приклад
– не проініціалізований покажчик:

/ * Ця програма містить помилку. * /


int main (void) {
int x, *p;
x = 10;
*р = x; / * Помилка, p не ініціалізований * /
return 0;
}

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


Розглянемо, чому це відбувається. Хоча вказівнику р не було присвоєно ніякого
значення, але в момент виконання операції *р = х він все ж мав деяке (абсолютно
довільне!) значення. Тому тут мала місце спроба виконати операцію запису в
область пам'яті, на яку вказував даний покажчик. У невеликих програмах така
помилка часто залишається непоміченою, тому що якщо програма і дані займають
небагато місця, то "постріл навмання" швидше за все буде "промахом". Зі
збільшенням розміру програми ймовірність "попасти" в неї зростає.
В такому простому випадку більшість компіляторів виводять попередження про
те, що використовується неініціалізований покажчик. Однак подібна помилка може
статися і в більш завуальованому вигляді, коли компілятор не зможе розпізнати її.
Друга поширена помилка полягає в простому непорозумінні при використанні
покажчика:

/ * Ця програма містить помилку. * /


#include <stdio.h>
int main (void) {
int x, *p;
x = 10;
p = x;
printf ("%d", *p);
return 0;
}

Виклик printf() не виводить на екран значення х, що дорівнює 10. Виводиться


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

p = x;

записаний неправильно. Він присвоює значення 10 покажчику, однак покажчик


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

p = &x;

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


попередження, але, як і в попередньому прикладі, компілятор не зможе розпізнати
цю помилку в більш завуальованому вигляді.
Ще одна типова помилка відбувається іноді при неправильному розумінні
принципів розташування змінних в пам'яті. Програмісту нічого не відомо про те, як
використовувані ним дані розташовуються в пам'яті, чи будуть вони розташовані так
само при наступному виконанні програми або як їх розташують інші компілятори.
- 88 -
Тому порівнювати одні покажчики з іншими на більше чи менше неприпустимо.
Наприклад, програма

char s[80], y[80];


char *p1, *p2;
p1 = s;
p2 = y;
if (p1 < p2) . . .

в загальному випадку неправильна.


Схожа помилка виникає, коли робиться необґрунтоване припущення про
розташування масивів. Іноді, припускаючи, що масиви розташовані поруч,
намагаються звертатися до них за допомогою одного і того ж покажчика, наприклад:

int first[10], second[10];


int *p, t;
p = first;
for (t = 0; t < 20; ++t) *p++ = t;

Так присвоювати значення масивам first і second не можна. Якщо компілятор


розмістить масиви поруч, це може і не привести до невірного результату. Однак
подібна помилка особливо неприємна тим, що під час відлагоджування вона може
залишитися непоміченою, а потім інший компілятор буде розміщувати масиви по-
іншому і програма виконається неправильно.
У наступній програмі наведено приклад дуже небезпечної помилки.
Намагайтеся самі знайти її, не підглядаючи в наступне пояснення.

Приклад 6.11

/* Це програма з помилкою. */
#include <string.h>
#include <stdio.h>

int main(void) {
char *p1;
char s[80];

p1 = s;
do {
gets(s); /* читання рядка*/

/* друк коду (десяткового еквіваленту)


кожного символа */
while (*p1)
printf("%d", *p1++);

} while (strcmp(s, "виконано"));

return 0;
}
- 89 -
Програма друкує значення символів ASCII, які знаходяться в рядку s. Друк
здійснюється за допомогою p1, що вказує на s. Помилка полягає в тому, що
вказівнику p1 присвоюється значення s тільки один раз, перед першим циклом do
while. У першій ітерації p1 правильно проходить по символам рядка s, проте вже в
наступній ітерації він починає не з першого символу, а з того, яким закінчив в
попередній ітерації do while. Так що в другій ітерації p1 може вказувати на середину
другого рядка, якщо другий рядок буде довшим першого, або ж взагалі на кінець
залишку ще першого рядка. Виправлена версія програми повинна виглядати так:

/* Це правильна програма. */
#include <string.h>
#include <stdio.h>

int main(void) {
char *p1;
char s[80];

do {
p1 = s; /* встановлення p1 в початок рядка s */
gets(s); /* зчитування рядка*/

/* друк коду (десяткового еквіваленту)


кожного символа */
while (*p1)
printf ("%d", *p1++);

} while (strcmp(s, "виконано"));

return 0;
}

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


рядка s. Про це необхідно завжди пам'ятати при повторному використанні
покажчиків.
Те, що неправильні покажчики можуть бути дуже "підступними", не може служити
причиною відмови від їх використання. Слід лише бути обережним і уважно
аналізувати кожне застосування покажчика в програмі.
- 90 -

ТЕМА 7. ФУНКЦІЇ В МОВІ СІ

7.1. Поняття функції та область її дії


Фу́нкція - це певний (пойменований) фрагмент програмного коду (процедура,
підпрограма), до якого можна звернутися при виконанні будь-якого іншого
програмного коду. З ім'ям функції нерозривно пов'язана адреса першої команди
(оператора), що входить у функцію, якій передається управління при зверненні до
функції. Тобто, ім'я функції є адресою першого виконуваного оператора функції.
Після виконання коду функції, управління виконанням програми повертається назад
на адресу повернення – наступний оператор програми, після оператору виклику
функції.
Функція може приймати параметри і може повертати деякі значення. Функція
повинна бути оголошена і відповідним чином визначена у програмі до першого
звертання до неї. У загальному вигляді визначення функції виглядає наступним
чином:

< тип_ДП> ім’я-функції (<список_формальних_параметрів>) {


тіло функції
}

де:

тип_ДП - визначає тип даного, що повертається функцією, який також
називається результатом виконання функції. Функція може повертати будь-
який тип даних, за винятком агрегатних (масиви, структури і т.ін.);
 список_формальних_параметрів - це список, елементи якого
відокремлюються один від одного комами. Кожен такий елемент
складається з імені змінної і її типу даних (наприклад, int iX, double pY). При
виклику функції формальні параметри набувають значення фактичних
аргументів, які передаються функції. Функція може бути і без формальних
параметрів (надалі, просто параметри), тоді їх список буде порожнім. Такий
порожній список можна вказати в явному вигляді, розмістивши всередині
дужок ключове слово void.
При оголошенні (деклараціях) змінних можна оголосити (декларувати) кілька
змінних одного і того ж типу, використовуючи для цього список одних тільки імен,
елементи якого відокремлені один від одного комами. На відміну від цього, всі
параметри функцій, навпаки, повинні оголошуватися окремо, причому для кожного
з них треба вказувати і тип, і ім'я. Тобто в загальному вигляді перелік оголошень
параметрів повинен виглядати наступним чином:

f (тип ім’я_змінної1, тип ім’я_змінної2,..., тип ім’я_змінноїN) {}

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


а друге - ні:

f (int i, int k, int j) / * правильне * /


f (int i, k, float j) / * неправильне, у змінної k повинен бути
власний специфікатор типу * /

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


чи бачить фрагмент коду програми інший фрагмент коду або даних, або чи має він
доступ до цього іншого фрагмента. Як визначаються області видимості в мові Сі,
- 91 -
говорилося в попередніх лекціях. Зараз же більш детально розглянемо одну
спеціальну область видимості - ту, яка визначається функцією.
Кожна функція являє собою завершений блок коду. Таким чином, вона визначає
область видимості цього блоку. Це означає, що код функції є закритим і
недоступним з будь-якої іншої функції, якщо тільки не виконується виклик функції,
що його містить. (Наприклад, не можна перейти в середину іншої функції за
допомогою оператора goto.) Код, який становить тіло функції, прихований від іншої
частини програми, і якщо він не використовує глобальних змінних, то не може
впливати на інші частини програми або, навпаки, бути під впливом з їхнього боку.
Інакше кажучи, код і дані, визначені усередині однієї функції, не можуть прямо
впливати на код і дані всередині іншої функції, так як у будь-яких двох різних
функцій різні області видимості і вони є непересічними.
Змінні, визначені всередині функції, є локальними. Локальна змінна
створюється на початку виконання функції, а при виході з цієї функції вона
знищується. Таким чином, локальна змінна не може зберігати своє значення в
проміжках між викликами функції. Єдиний виняток з цього правила - локальні змінні,
що оголошені зі специфікатором класу пам'яті static. Таким змінним пам'ять
виділяється так само, як і глобальним, які використовуються для зберігання
значень, але область видимості змінних static обмежена функціями, у яких вони
містяться.
Формальні параметри функції завжди знаходяться в її області видимості. Це
означає, що параметр завжди доступний всередині всієї функції. Параметр
створюється на початку виконання функції, і знищується при виході з неї.
У мові Сі функцію НЕ можна визначати всередині іншої функції. Тому Сі
практично не є мовою з блочною структурою. ВСІ функції в мові Сі є глобальними.

7.2. Аргументи функції


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

/ * Повертає 1, якщо символ 'c' входить в рядок s;


і 0 в іншому випадку. * /
int is_in (char* s, char c) {
while (*s)
if (*s == c)
return 1;
else
s++;
return 0;
}

Функція is_in() має два параметри: s і d. Якщо символ 'c' входить в рядок s, то ця
функція повертає 1, в іншому випадку вона повертає 0.
Хоча параметри виконують спеціальне завдання, - приймають значення
аргументів, переданих функції, - вони все одно поводяться так, як і інші локальні
змінні. Формальним параметрам функції, наприклад, можна присвоювати будь-які
значення або використовувати ці параметри в будь-яких виразах в середині функції.
Виклики за значенням і за посиланням
У мовах програмування є два способи передачі значень підпрограмі (не
включаючи використання глобальних змінних!). Перший з них - виклик за
- 92 -
значенням. При його застосуванні у формальний параметр підпрограми копіюється
значення аргументу. У такому разі можливі зміни параметра викликаною
підпрограмою на аргумент не впливають.
Другим способом передачі аргументів підпрограмі є виклик за посиланням. При
його застосуванні в параметр копіюється адреса аргументу, тобто покажчик на
аргумент. Це означає, що, на відміну від виклику за значенням, зміни значення
параметра викликаною підпрограмою призводять до точно таких самих змін
значення самого аргументу, оскільки виконуються не на копії, а на самому аргументі.

Виклик за значенням

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


виклик за значенням. Зазвичай це означає, що код, що знаходиться всередині
функції, не може змінювати значень аргументів, які використовувалися при
виконанні функції. Проаналізуємо таку програму:

Приклад 7.01

#include <stdio.h>

int sqr (int x);

int main(void) {
int t=10;
printf ("%d%d", sqr(t), t);
return 0;
}

int sqr (int x) {


x = x*x;
return(x);
}

У цьому прикладі в параметр х копіюється 10 - значення аргументу для sqr(). Коли


виконується присвоювання х = х * х, модифікується тільки локальна змінна х. А
значення змінної t, використаної в якості аргументу при виклику sqr(), як і раніше
залишається рівним 10. Тому виведено буде наступне: 100 10.
Слід пам'ятати, що саме копія значення аргументу передається у функцію. А
те, що відбувається всередині функції, не впливає на значення змінної, яка була
використана при виклику в якості аргументу.

Виклик за посиланням

Хоча в Сі для передачі параметрів застосовується лише механізм виклику за


значенням, можна емулювати виклик і за посиланням, передаючи не сам
аргумент, а покажчик на нього. Звичайно, при самій передачі покажчика буде
застосований виклик за значенням, і саму адресу покажчика на аргумент всередині
функції змінити буде не можна. Однак для того об'єкта, на який вказує цей покажчик,
все станеться так, ніби цей об'єкт був переданий за посиланням. У деяких мовах
програмування (наприклад, в Паскаль) були спеціальні засоби, що дозволяють
уточнити, як слід передавати аргументи: за посиланням або за значенням. Мова Сі
- 93 -
уніфікує цей механізм і робить його однаковим. Так, як функції передається адреса
аргументу, то її внутрішній код в змозі змінити значення цього аргументу, який, між
іншим, знаходиться за межами самої функції.
Покажчик передається функції так, як і будь-який інший аргумент. Звичайно, в
такому випадку параметр слід декларувати як один з типів покажчиків. Це можна
побачити на прикладі функції swap(), яка міняє місцями значення двох цілих змінних,
на які вказують аргументи цієї функції:

void swap(int* x, int* y) {


*x = *x + *y; /* x=a+b, y=b */
*y = *x - *y; /* x=a+b, y=a */
*x = *x - *y; /* x=b, y=a */
}

Функція swap() може виконувати обмін значеннями двох змінних, на які вказують х і
y, тому що передаються їх адреси, а не значення. Усередині функції,
використовуючи стандартні операції з вказівниками, можна отримати доступ до
вмісту змінних і провести обмін їх значень.
Необхідно пам'ятати, що викликати swap() (або будь-яку іншу функцію, в якій
використовуються параметри у вигляді вказівників) слід разом з адресами
аргументів. Наступна програма показує, як треба правильно викликати swap():

Приклад 7.02

#include <stdio.h>
void swap(int* x, int* y);
int main(void) {
int i, j;
i = 10;
j = 20;
printf ("i та j перед обміном значеннями:% d% d \ n", i, j);
swap (&i, &j); / * Передати адреси змінних i і j * /
printf ("i та j після обміну значеннями:% d% d \ n", i, j);
return 0;
}

void swap(int* x, int* y) {


*x = *x + *y; /* x=a+b, y=b */
*y = *x - *y; /* x=a+b, y=a */
*x = *x - *y; /* x=b, y=a */
return;
}

І ось що вивела ця програма:

i та j перед обміном значеннями: 10 20


i та j після обміну значеннями: 20, 10

У головній програмі змінній i присвоюється значення 10, а змінній j - значення 20.


Потім викликається функція swap() з адресами цих змінних (Для отримання адреси
кожної з змінних використовується унарний оператор &). Тому в swap() передаються
адреси змінних i та j, а не їх значення.
- 94 -
Як вчинити, якщо є необхідність передати в функцію вказівник на об'єкт, але
немає необхідності в зміні цього об'єкта функцією? В цьому випадку слід
використовувати кваліфікатор const при описі формальних параметрів в заголовку
оголошуваної функції. Наприклад,

int func3 (int count, char* str1, const char* str2);

функція func3, що повертає ціле значення, приймає кілька параметрів. Параметр


count передається за значенням і відповідний аргумент не може бути змінений, а
ось рядки str1 і str2 передаються за посиланням, і відповідно, можуть бути змінені
функцією func3. Щоб цього не сталося стосовно рядка str2 його формальний
параметр оголошений з кваліфікатором типу const. Подібного роду оголошення
формальних параметрів з кваліфікаторіом const слід використовувати завжди при
передачі адрес і посилань в функцію, якщо немає необхідності в зміні відповідного
аргумента, для запобігання випадкової такої зміни.

7.3. Передача масивів у функцію


У мові Сі можна передати весь масив, як аргумент функції. Коли в якості
аргументу функції використовується ім'я масиву без індексу, то функції ВЖЕ
передається його адреса, і додаткового використання унарного оператора &
отримання адреси не потрібно. У цьому полягає виняток стосовно правила, яке
свідчить, що при передачі параметрів в Сі завжди використовується виклик за
значенням. У разі передачі масиву функції її внутрішній код працює з реальним
вмістом цього масиву і цілком може змінити цей вміст. Наприклад, в програмі
нижче в func1() передається покажчик на масив i:

int main(void) {
int i[10];
func1(i);
/* ... */
}

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


функції його можна оголосити одним з трьох варіантів:
 як масив певного розміру;
 як безрозмірний масив;
 як покажчик.
Наприклад, щоб функція func1() отримала доступ до значень, що зберігаються
в масиві i, вона може бути оголошена як:

Приклад 7.03

void func1(short int x[SIZE]) /* як масив певного розміру */


{
/* ... */
}

void func1(short int x[], int size) /* рекомендований спосіб передачі


одновимірних масивів у функцію – як масив
невизначеного розміру */
{
- 95 -
/* ... */
}

void func1(short int* x, int size) /* як покажчик */


{
/* ... */
}

Ці три оголошення тотожні, тому що кожне з них повідомляє компілятору одне і


те ж: в функцію буде переданий покажчик на змінну цілого типу. У першому
оголошенні використовується стандартне оголошення масиву, у третьому -
покажчик. В другому прикладі змінена форма оголошення масиву повідомляє
компілятору, що в функцію буде переданий масив невизначеної довжини. Як видно,
довжина масиву не має для функції ніякого значення, тому що в Сі перевірка меж
масиву не виконується.
Якщо двовимірний масив використовується в якості аргумента функції, то
до неї передається тільки покажчик на початковий елемент масиву, тобто на
елемент з індексом (0,0). У відповідному параметрі функції, який отримує
двовимірний масив, обов'язково повинен бути зазначений розмір правого виміру,
що дорівнює довжині рядка масиву. Розмір лівого виміру вказувати не обов'язково.
Розмір правого виміру необхідний компілятору для того, щоб усередині функції
правильно обчислити адресу елемента масиву, так як для цього компілятор повинен
знати довжину рядка масиву. Наприклад, функція, яка отримує двомірний масив
цілих розмірністю 10 × 10, може бути оголошена так:

void func1 (int x[][10])


{
/* ... */
}

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


правильно обчислити адресу елемента масиву. Якщо при компіляції функції це
невідомо, то неможливо визначити, де починається наступний рядок, і обчислити,
наприклад, адресу елемента x[2][4].
Передаючи багатовимірний масив у функцію, в оголошенні параметрів функції
необхідно вказати всі розміри вимірів, крім крайнього лівого. Наприклад, якщо
масив m оголошений у програмі як

int m[4] [3] [6] [5];

то функція, що приймає цей масив, повинна бути оголошена приблизно так:

void func1(int d[][3][6][5])


{
/* ... */
}

Звичайно, можна включити в оголошення і розмір 1-го виміру, але це зайве. Приклад
використання такої технології передачі двовимірного масива наведено нижче:

Приклад 7.4(1)
- 96 -

#include "stdlib.h"
#include "stdio.h"

#define SIZE1 10
#define SIZE2 10

void outputX(int x[][SIZE2], int size1, int size2) {


for (int i = 0; i < size1; ++i) {
for (int j = 0; j < size2; ++j)
printf("%d ", x[i][j]);
printf("\n");
}
return;
}

int main(void) {
int x[SIZE1][SIZE2]; /* оголошення масиву SIZE1xSIZE2 цілих */

/* присвоєння елементам масиву значень від 0 до SIZE1*SIZE2 - 1 */


for (int i = 0; i < SIZE1; ++i)
for (int j = 0; j < SIZE2; ++j)
x[i][j] = i*SIZE2 + j;

/* вивід на екран масиву x */


outputX(x, 10, 10);

return 0;
}

Такий спосіб передачі двовимірного (багатовимірного) масиву у функцію є


рекомендованим.

Другий спосіб передачі двовимірного масива (як параметра) до функції


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

Приклад 7.04(2)

#include "stdlib.h"
#include "stdio.h"
#include "conio.h"

#define SIZE1 10
#define SIZE2 10

void outputX(int* x, int size1, int size2) {


for (int i = 0; i < size1; ++i) {
for (int j = 0; j < size2; ++j)
printf("%d ", x[i*size2+j]);
printf("\n");
}
return;
}

int main(void) {
int x[SIZE1][SIZE2]; /* оголошення масиву SIZE1xSIZE2 цілих */

/* присвоєння елементам масиву значень від 0 до SIZE1*SIZE2 - 1 */


for (int i = 0; i < SIZE1; ++i)
for (int j = 0; j < SIZE2; ++j)
- 97 -
x[i][j] = i*SIZE2 + j;

/* вивід на екран половини масиву x */


outputX((int*) x, 5, 10); // тут можна записати список аргументів і так:
(&x[0][0], 5, 10)

return 0;
}

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


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

7.4. Аргументи функції main(): argv та argc


Іноді, при запуску програми буває корисно передати їй будь-яку інформацію у
командному рядку одночасно із запуском самої програми. Зазвичай така інформація
передається функції main() за допомогою аргументів командного рядка. Аргумент
командного рядка - це інформація, яка вводиться в командному рядку операційної
системи слідом за ім'ям програми. Наприклад, щоб запустити компіляцію програми,
необхідно в командному рядку після підказки > набрати приблизно наступне:

cc назва_вихідного_файла_програми

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


програми, яку збираються компілювати.
Щоб прийняти аргументи командного рядка, використовуються два спеціальних
наперед відомих вбудованих аргумента: argc і argv. Параметр argc містить кількість
аргументів у командному рядку і є цілим числом, причому він завжди не менший 1,
тому що першим (нульовим!) аргументом вважається ім'я самої програми. А
параметр argv є іменем масиву покажчиків на рядки, що містять кожний по одному
аргументу з командного рядка. У цьому масиві кожен елемент вказує на відповідний
йому за номером рядок аргумента командного рядка. Всі аргументи командного
рядка є рядками символів, тому перетворення будь-яких чисел, що являють собою
аргументи, в потрібний двійковий формат має бути передбачено в програмі при її
розробці.
Ось простий приклад використання аргументу командного рядка. На екран
виводиться слово «Привіт» і ім'я, яке повинне мати вигляд аргументу командного
рядка.

Приклад 7.05

#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[]) {
if(argc!=2) {
printf("Ви забули ввести своє ім’я.\n");
exit(1);
}
printf("Привіт %s", argv[1]);
return 0;
}
- 98 -
У багатьох середовищах всі аргументи командного рядка необхідно
відокремлювати один від одного пробілом або табуляцією. Коми, крапки з комою і
подібні символи тут розділювачами аргументів не вважаються. Наприклад,

run Spot, run

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

Павло,Юля,Саша

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


вважаються.
Якщо в рядку є пробіли, то, щоб з нього не вийшло кілька аргументів, в деяких
середовищах цей рядок можна брати в подвійні лапки. В результаті весь такий
"олапчаний" рядок буде вважатися одним аргументом.
Дуже важливо правильно оголошувати argv. Ось як це роблять частіше за все:

char* argv[];

Порожні квадратні дужки вказують на те, що у масива невизначена довжина. Тепер


отримати доступ до окремих аргументів можна за допомогою індексації масиву argv.
Наприклад, argv[0] вказує на нульовий символьний рядок, яким завжди є ім'я
програми, що запускається; argv[1] вказує на перший аргумент і так далі.
Іншим прикладом використання аргументів командного рядка є наведена далі
програма countdown (рахунок в зворотньому порядку). Ця програма рахує в
зворотньому порядку, починаючи з будь-якого значення (зазначеного в командному
рядку), і подає звуковий сигнал, коли доходить до 0. Перший аргумент, що містить
початкове значення, перетворюється з рядка в ціле значення за допомогою
стандартної функції atoi(). Якщо другим аргументом командного рядка (а якщо
вважати аргументом ім'я програми, то третім) є рядок "display" (вивід на екран), то
результат відліку (в зворотньому порядку) буде виводитися на екран.

Приклад 7.06

/* Програма зворотнього відліку. */


#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>

int main(int argc, char *argv[]) {


int disp, count;

if (argc<2) {
printf ("У командному рядку необхідно ввести число, з якого \ n ");
printf ("починається відлік. Спробуйте знову. \ n");
exit (1);
}

if (argc==3 && !strcmp(argv[2], "display"))


disp = 1;
else
- 99 -
disp = 0;

for (count=atoi(argv[1]); count; --count)


if (disp) printf("%d\n", count);

putchar('\a'); /* тут подається звуковий сигнал */


printf("Відлік закінчено");
return 0;
}

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


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

Приклад 7.07

#include <stdio.h>
int main(int argc, char *argv[]) {
int t, i;
for(t=0; t<argc; ++t) {
i = 0;
while(argv[t][i]) {
putchar(argv[t][i]);
++i;
}
printf("\n");
}
return 0;
}

Слід пам'ятати, що перший індекс argv забезпечує доступ до рядка, а другий


індекс - доступ до його окремих символів.
Зазвичай argc і argv використовують для того, щоб передати програмі початкові
команди, які знадобляться їй при запуску. Наприклад, аргументи командного рядка
часто вказують такі дані, як ім'я файлу, з яким повинна працювати програма,
додатковий параметр або альтернативну поведінку. Використання аргументів
командного рядка надає програмі "професійний зовнішній вигляд" і полегшує її
використання в пакетних файлах.
Імена argc і argv є традиційними, але не обов'язковими. Ці два параметри у
функції main() ви можете назвати як завгодно, тобто вони розпізнаються не за
іменем, а за їх позицією у списку параметрів функції main(). Крім того, у деяких
компіляторах для main() можуть підтримуватися додаткові аргументи.
Коли для програми не потрібні параметри командного рядка, то найчастіше явно
декларують функцію main() як таку, що не має параметрів. В такому випадку у списку
параметрів цієї функції використовують ключове слово void.
- 100 -

7.5. Оператор return

Повернення значення функцією

Всі функції, крім тих, які відносяться до типу void, повертають значення. Це
значення потрібно вказати виразом в операторі return. У функції, тип якої відмінний
від void, в операторі return необхідно обов'язково вказати значення, що
повертається. Тобто, якщо для будь-якої функції зазначено, що вона повертає
значення, то всередині цієї функції у будь-якого оператора return обов'язково має
бути свій вираз. Якщо функція, <тип> якої відмінний від void, виконується до самого
кінця (тобто до фігурної дужки, що її закриває) і відсутній оператор return зі
значенням <тип>, то повертається довільне (непередбачуване з точки зору
розробника програми!) значення. Хоча тут немає синтаксичної помилки, це є
серйозним упущенням і таких ситуацій необхідно уникати.
Якщо функція не оголошена як така, що має тип void, вона може
використовуватися як операнд у виразі. Тому кожен з наступних виразів є
правильним:

x = power(y);
if (max(x,y) > 100)
printf("більше");
for (ch=getch(); isdigit(ch); ) ... ;

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


оператора присвоювання, тобто виклик функції не може бути виразом lvalue. Вираз

swap (x, y) = 100; / * Неправильний вираз * /

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


позначить його як помилковий і виведе помилку.
У програмі можна використовувати функції трьох видів.
Перший вид - прості обчислення. Ці функції призначені для виконання
операцій над своїми аргументами і повертають отримане в результаті цих операцій
значення. Обчислювальна функція є функцією "в чистому вигляді". Як приклади
можна навести стандартні бібліотечні функції sqrt() і sin(), які обчислюють
квадратний корінь і синус свого аргументу відповідно.
Другий вид включає до себе функції, які обробляють інформацію і
повертають значення, яке показує, чи успішно була виконана ця обробка.
Прикладом є бібліотечна функція scanf(), яка вводить значення з клавіатури. Якщо
операція введення була завершена успішно, функція повертає кількість введених
змінних, а в разі помилки вона повертає EOF (end of file, -1).
У функцій останнього, третього виду, взагалі немає значень, що явно
повертаються. По суті, такі функції є чисто процедурними і ніяких значень видавати
не повинні. Прикладом є exit(), яка припиняє виконання програми. Всі функції, які не
повертають значення, повинні оголошуватися як такі, що повертають значення типу
void. Оголошуючи функцію як ту, що повертає значення типу void, забороняється її
подальше застосування в виразах, запобігаючи таким чином випадковому
використанню цієї функції не за призначенням.
Іноді функції, які, здавалося б, практично не видають змістовний результат,
все ж повертають якесь значення. Наприклад, printf() повертає кількість виведених
символів. Іншими словами, хоча всі функції, за винятком тих, що відносяться до типу
void, повертають значення, зовсім не слід прагнути обов'язково використовувати ці
- 101 -
значення. Присвоєння аж ніяк не є обов'язковим, причому відсутність його не стане
причиною будь-яких неприємностей. Якщо значення, що повертається, не входить
ні в один з операторів присвоювання, то це значення буде просто відкинуто.
Відзначимо також, що таке відкидання зустрічається дуже часто.

Покажчики, що повертаються

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


відносяться до особливого типу даних. Така особливість покажчиків визначається
тим, що арифметика покажчиків (адресна арифметика) працює з урахуванням
розміру базового типу. Наприклад, якщо покажчику на ціле надати мінімальне
(одиничне) збільшення, то його поточне значення стане на чотири більше, ніж
попереднє (за умови, що цілі значення займають 4 байта). Взагалі, кожен раз, коли
значення покажчика збільшується (зменшується) на мінімальну величину, то він
вказує на наступний (попередній) елемент, який має базовий тип покажчика. Так як
розміри різних типів даних можуть бути різними, то компілятор повинен знати тип
даних, на які може вказувати покажчик. Тому в оголошенні функції, яка повертає
покажчик, тип покажчика, що повертається, повинен декларуватися явно.
Наприклад, не можна оголошувати тип, що повертається, як int*, якщо
повертається покажчик типу char* ! Іноді (щоправда, вкрай рідко!) потрібно, щоб
функція повертала "універсальний" покажчик, тобто покажчик, який може вказувати
на дані будь-якого типу. Тоді тип результату функції слід визначити як void*.
Щоб функція могла повернути покажчик, вона повинна бути оголошена як така,
що повертає покажчик на потрібний тип. Наприклад, наступна функція повертає
покажчик на перше входження символу, що задається змінною с, в рядок s. Якщо
цього символу в рядку немає, то повертається покажчик на символ кінця рядка ('\
0').

/* Повертає покажчик на перше входження c в s. */


char* match(char c, char* s) {
while((c!=*s) && *s)
s++;
return(s);
}

Ось невелика програма, в якій використовується функція match():

Приклад 7.08

#include <stdio.h>

char* match(char c, char* s); /* прототип */

int main(void) {
char s[81], *p, ch;

printf("Введіть рядок: ");


gets_s(s);

printf("\nВведіть символ: ");


ch = _getche();
p = match(ch, s);
- 102 -
if (*p) /* символ знайдено */
printf("\n%s\n", p);
else
printf("\nСимволу немає\n");
return 0;
}

Ця програма спочатку зчитує рядок, а потім символ. Потім проводиться пошук


місцезнаходження символу в рядку. При наявності символу ch в рядку змінна p
вкаже на нього, і програма виведе рядок, починаючи зі знайденого символу. Якщо
символ в рядку не знайдений, то p вкаже на символ кінця рядка ("\0"), причому *p
представлятиме логічне значення ХИБНОГО (false). В такому випадку програма
виведе повідомлення "Символу нема".

Функція типу void

Одним із застосувань ключового слова void є явне оголошення функцій, які не


повертають значень. Ми вже знаємо, що такі функції не можуть застосовуватися в
виразах, і вказівка ключового слова void запобігає їх випадковому використанню не
за призначенням. Наприклад, функція print_vertical() виводить в бічній частині
екрана свій рядковий аргумент по вертикалі зверху вниз.
Ось приклад використання функції print_vertical():

#include <stdio.h>
void print_vertical(char* str); /* прототип */

int main(int argc, char* argv[]) {


if (argc > 1) print_vertical(argv[1]);
return 0;
}

void print_vertical(char* str) {


while(*str)
printf("%c\n", *str++);
}

7.6. Прототип функції


У сучасних, правильно написаних програмах на мові Сі, кожну функцію перед
використанням необхідно оголошувати. Зазвичай це робиться за допомогою
прототипу функції. У первинному варіанті мови Сі прототипів не було, але вони були
введені вже в Стандарт С89. Хоча прототипи формально не потрібні, але їх
використання є обов'язковим у сучасному програмуванні. Прототипи дають
компілятору можливість ретельніше виконувати перевірку типів, подібно до того, як
це робиться в таких мовах як Pascal. Якщо використовуються прототипи, то
компілятор може виявити будь-які сумнівні перетворення типів аргументів, необхідні
при виконанні функції, якщо тип її параметрів буде відрізнятися від типів аргументів.
При цьому будуть видані попередження про всі такі сумнівні перетворення.
Компілятор також виявить відмінності в кількості аргументів, використаних при
виконанні функції, і в кількості параметрів функції.
У загальному вигляді прототип функції повинен виглядати таким чином:

тип ім’я_функції (тип ім’я_парам1, тип ім’я_парам2, ..., тип ім’я_парамN);


- 103 -

Використання імен параметрів не є обов'язковим. Однак вони дають можливість


компілятору при наявності помилки вказати імена, для яких виявлено
невідповідність типів, тому, для полегшення подальшого відлагоджування програми
слід ці імена обов'язково вказувати у прототипі.
Наступна програма показує, наскільки цінними є прототипи функцій. У ній
виводиться повідомлення про помилку, що відбувається через те, що програма
містить спробу виклику функції sqr_it() з цілим аргументом, в той час як потрібний
вказівник на ціле.

Приклад 7.10

/* У цій програмі використовується прототип функції


щоб забезпечити ретельну перевірку типів. * /

void sqr_it(int* i); /* прототип */

int main(void) {
int x;
x = 10;
sqr_it(x); /* невідповідність типів */
return 0;
}

void sqr_it(int *i)


{*i = *i * *i;}

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


знаходиться в програмі до першого виклику цієї функції. Хоча визначення функції і
може служити її прототипом в малих програмах, але у великих таке зустрічається
рідко - особливо, коли використовується кілька файлів. Єдина функція, для якої не
потрібно прототип - це main(), так як це перша функція, що викликається на початку
роботи програми.
Є невелика, але важлива різниця в тому, як саме в Сі і C++ обробляється
прототип функції, що не має параметрів. У Cі++ порожній список параметрів
вказується повною відсутністю в прототипі будь-яких параметрів. наприклад,

int f (); / * Прототип C++ для функції, яка не має параметрів * /

Однак в Сі цей оператор означає щось інше. Через необхідність дотримуватися


сумісності з початковою версією Сі порожній список параметрів повідомляє, що
просто про параметри не надано жодної інформації. Що стосується компілятора Сі,
то для нього ця функція може мати декілька параметрів, а може не мати й жодного.
Такий оператор називається старомодним оголошенням функції.
Тому, якщо функція в мові Сі не має параметрів, то в її прототипі всередині
списку параметрів необхідно обов'язково вказувати ключове слово void. Ось,
наприклад, прототип функції f() в тому вигляді, в якому він повинен бути в програмі
на мові Сі:

float f (void);
- 104 -
Таким чином, компілятор дізнається, що у функції немає параметрів, і будь-яке
звернення до функції, в якому є аргументи, буде вважатися помилкою. У C++
використання ключового слова void всередині порожнього списку параметрів також
дозволено, але вважається зайвим.
Прототипи функцій дозволяють "відловлювати" помилки ще до запуску
програми. Крім того, вони забороняють виклик функції при розбіжності типів (тобто
з невідповідними аргументами) і тим самим допомагають перевіряти правильність
програми.
І наостанок, слід зазначити таке: так, як в ранніх версіях Сі синтаксис прототипів
в повному обсязі не підтримувався, то в Сі прототипи формально не обов'язкові.
Такий підхід необхідний для сумісності з Сі-кодом, створеним ще до появи
прототипів. Але якщо старий Сі-код переноситься в C++, то перед компіляцією цього
коду в нього необхідно додати повні прототипи функцій. Слід пам'ятати, що хоча
прототипи в Сі не є обов'язковими (для С89), але вони є обов'язковими для С99 і
подальших версій і C++. Це означає, що кожна функція в програмі на мові C++
повинна мати повний прототип. Тому при написанні програм на Сі в них вказуються
повні прототипи функцій – саме так чинить більшість програмістів, які працюють на
цій мові.
Будь-яка стандартна бібліотечна функція в програмі повинна мати прототип.
Тому для кожної такої функції необхідно ввести до програми відповідний файл
заголовку. Всі необхідні заголовки надаються компілятором Сі. В системі
програмування на мові Сі бібліотечними заголовками (зазвичай) є файли, в іменах
яких використовується розширення .h. У заголовку є два основних елементи: будь-
які визначення, що використовуються бібліотечними функціями, і прототипи
бібліотечних функцій. Наприклад, майже в усі програми курсу лекцій включається
файл <stdio.h>, тому що в цьому файлі знаходиться прототип для printf().

7.7. Аргументи за замовчуванням (С++)


Щоб спростити виклик функції, в її заголовку можна задати значення параметрів
за замовчуванням. Ці параметри мають бути останніми у списку й можуть
опускатися при виклику функції. Якщо при виклику параметр є опущений, має бути
опущено й усі параметри, що стоять за ним. Як значення параметрів за
замовчуванням можуть використовуватися константи, глобальні змінні й вирази.
Розглянемо приклади прототипів функцій з параметрами за замовчуванням.

int f(int a, int b=0); // Параметр b має значення за замовчуванням 0.


void f1(int, int=100, char* = 0); /* Зверніть увагу на пробіл між * й =, без нього б
вийшла операція складного присвоювання *= */
void err(int errValue = errno); //errno – глобальна змінна.

Варіанти виклику цих функцій:

f(100); // Виклик функції з першим параметром 100, другий за замовчуванням – 0.


f(А, 1); // Виклик функції з першим параметром – значенням змінної А, другим – 1.
f1(А); // Виклик функції з другим та третім параметрами за замовчуванням.
f1(a, 10); // Виклик функції зі значенням третього параметра за замовчуванням.
f1(a, 10, "Hello"); // Виклик без значень параметрів за замовчуванням.

Приклад 7.11. Написати функцію, яка виконує ділення одного цілого числа на друге.
Якщо друге число не зазначено, то виконується ділення на 2. Тексти функції та її
виклику в основній програмі:
- 105 -
double divide(int a, int b=2) {
return (double)a/b;
}

void main(void) {
printf ("%f\n", divide(12)); // 12 / 2
printf ("%f",divide(20,4)); // 20 / 4
getch();
}

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

6
5

7.8. Перевантаження функцій (C++)


Часто буває зручно, щоб функції, які реалізовують однаковий алгоритм по
різному для різних типів даних, мали однакове ім’я. Якщо це ім’я несе певну
інформацію, це робить програму більш зрозумілою, оскільки для кожної дії треба
пам’ятати лише одне ім’я. Використовування кількох функцій з однаковим ім’ям, але
з різними типами параметрів називається перевантаженням функцій.
Компілятор визначає, яку саме функцію слід викликати, за типом фактичних
параметрів. Цей процес називається дозволом перевантаження (переклад з
англійського слова “resolution”). Тип значення, яке повертає функція, у дозволі не
бере участі. Механізм дозволу ґрунтується на доволі складному наборі правил,
зміст яких зводиться до того, щоб використати функцію з найбільш придатними
аргументами й видати повідомлення, якщо така не знайдеться. Мета
перевантаження – зробити так, щоб функції з однаковим ім’ям виконувалися по-
різному (і за можливості - повертали різні значення) при звертанні до них з різними
за типами й кількістю параметрами. За приклад наведемо чотири варіанти функції
для визначення рівності двох аргументів одне одному:

Приклад 7.12

#include "locale.h"
#include "string.h"
#include "windows.h"

int Equal(int i1, int i2);


int Equal(double d1, double d2);
int Equal(char c1, char c2);
int Equal(char* s1, char* s2);

int main(int argc, char* argv[])


{
int b;

SetConsoleCP(1251);
SetConsoleOutputCP(1251);

b = Equal(5, 6); // викликається Equal(int, int), b = 0


b = Equal(3.755, 3.755); // викликається Equal(double, double), b = 1
b = Equal(3.755, 3.7556); // викликається Equal(double, double), b = 0
b = Equal('A', 'A'); // викликається Equal(char, char), b = 1
b = Equal("Сьогодні", "Сьогодні"); // викликається Equal(char*, char*), b=1
b = Equal("Сьогодні", "Сьогодня"); // викликається Equal(char*, char*), b=0
- 106 -
return 0;
}

Усі чотири функції мають однакове ім’я Equal й відрізняються лише типами
параметрів (тому вважаються за різні функції). За викликання функції Equal()
компілятор обирає відповідно до типу фактичних параметрів варіант функції (у
наведеному прикладі буде послідовно викликано всі чотири варіанти функції). Якщо
точної відповідності не віднайдено, виконуються перетворювання порядкових типів
відповідно до загальних правил, наприклад char до int, float до double, тощо. Далі
виконуються стандартні перетворювання типів, наприклад int до double, покажчиків
до void*, тощо. Наступним кроком є виконання перетворювань типів, заданих
користувачем, а також пошук відповідностей за рахунок змінної кількості аргументів
функції. Якщо відповідність на тому самому етапі може бути набута у понад один
спосіб, виклик вважається за неоднозначний і видається повідомлення про помилку.
Неоднозначність може виникнути за:
- перетворення типу;
- використання параметрів-посилань;
- використання аргументів за замовчуванням.
Приклад неоднозначності за перетворення типу:

#include <stdio.h>
int f(int a) {return a;}
int f(int a, int b=1) {return a*b;}

void main () {
printf ("%d", f(10,2)); // Викликається f(int, int)
printf ("%d", f(10)); // Неоднозначність – що викликати: f(int, int) чи f(int)?
}
Слід відрізняти перевантаження функцій і редекларацію. Якщо функції з
однаковим ім’ям мають однакові параметри і типи результату, вважається, що друге
визначення розглядатиметься як редекларація (повторне визначення). Якщо
параметри функцій з однаковим ім’ям збігаються і функцій відрізняються лише
типом значення, яке повертається, це призведе до помилки.

Правила опису перевантажених функцій:


- перевантажені функції мають перебувати в одній області видимості, інакше
станеться приховування імен, аналогічне до однакових імен змінних у
вкладених блоках;
- перевантажені функції можуть мати параметри за замовчуванням, при цьому
значення одного й того самого параметра у різних функціях мають збігатися.
У різних варіантах перевантажених функцій може бути різна кількість
параметрів за замовчуванням;
- функції не можуть бути перевантажені, якщо опис їхніх параметрів
відрізняється лише специфікатором const чи використанням посилання (див.
п. 7.12), наприклад int та const int чи int та int&.
-
Приклад 7.13. Написати дві функції для обчислення суми елементів цілого й
дійсного масивів відповідно. Використати ці функції для обчислення сум двох
масивів.
Тексти функцій та їх викликів в основній програмі:

int adder (int iarray[7]) {


int isum = 0;
- 107 -
for(int i=0; i<7; i++)
isum += iarray[i];
return(isum);
}

float adder (float farray[7]) {


float fsum = 0;
for(int i= 0; i < 7; i++)
fsum += farray[i];
return (fsum) ;
}

void main () {
int iarray[7] = {5,1,6,20,15,0,12};
float farray[7] = {3.3,5.2,0.05,1.49,3.12345,31.0,2.007};
int isum; float fsum;
isum = adder (iarray); // Виклик для масиву цілих чисел
fsum = adder (farray); // Виклик для масиву дійсних чисел
printf ("Сума масиву цілих чисел дорівнює %d \n",isum);
printf ("Сума масиву дійсних чисел дорівнює %f \n",fsum);
getch ();
}

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

Сума масиву цілих чисел дорівнює 59


Сума масиву дійсних чисел дорівнює 46.1705

Приклад 7.14. Написати три функції для обчислення периметра трикутника,


чотирикутника й п’ятикутника відповідно.
Тексти функції та її виклику в основній програмі:

/* Завдання відрізняється від попереднього тим, що тип параметрів є однаковий,


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

// Визначення всіх потрібних версій функції


// Версія функції для трикутника
double Perimetr(double Len1, double Len2, double Len3)
{ return Len1 + Len2 + Len3; }

// Версія функції для чотирикутника


double Perimetr(double Len1, double Len2, double Len3, double Len4)
{ return Len1 + Len2 + Len3 + Len4; }

// Версія функції для п’ятикутника


double Perimetr(double Len1, double Len2, double Len3, double Len4, double Len5)
{ return Len1 + Len2 + Len3 + Len4 + Len5; }

int main() {
double AB = 5, BC = 3, CA = 7;
double CD = 8, DA = 11, DE = 2, EA = 9;
- 108 -
printf ("Perimetr ABC = %f \n", Perimetr(AB, BC, CA));
printf ("Perimetr ABCD = %f \n", Perimetr(AB,BC,CD,DA));
printf ("Perimetr ABCDE = %f \n", Perimetr(AB, BC, CD, DE, EA));
getch();
}

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

Perimetr ABC = 15
Perimetr ABCD = 27
Perimetr ABCDE = 27

7.9. Оголошення списків параметрів змінної довжини


Можна викликати функцію, яка має змінну кількість параметрів. Найвідомішим
прикладом є printf(), scanf(). Щоб повідомити компілятору, що функції буде передано
заздалегідь невідому кількість аргументів, оголошення списку її параметрів
необхідно закінчити трьома крапками. Наприклад, наступний прототип вказує, що у
функції func() буде як мінімум два цілих параметра і після них ще кілька (в тому числі
і 0) параметрів:

int func (int a, int b, ...);

Ще приклад з printf():

int printf(const char *format, ...);

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


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

int func (...); / * Помилка * /

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


Вказівники на функції - дуже потужний засіб мови Сі. Хоча не можна не
відзначити, що це дуже важкий для розуміння термін. Функція розташовується в
пам'яті за певною адресою, яка присвоюється покажчику (імені функції без списку
параметрів!) як його значення. Адресою функції є її точка входу (адреса найпершого
виконуваного оператора функції). Наприклад, для функції

int Equal(char c1, char c2) {


return (c1 == c2);
}

з Прикладу 7.12 точка входу може мати такий вигляд (якщо виконати дізасемблер
на об'єктному модулі програми):

Приклад 7.15 Асемблерний код функції int Equal(char c1, char c2) з Прикладу 7.12

int Equal(char c1, char c2)


{
011916D0 push ebp ; точка входу функції int Equal(char c1, char c2)
- 109 -
011916D1 mov ebp,esp
011916D3 sub esp,0C4h
011916D9 push ebx
011916DA push esi
011916DB push edi
011916DC lea edi,[ebp-0C4h]
011916E2 mov ecx,31h
011916E7 mov eax,0CCCCCCCCh
011916EC rep stos dword ptr es:[edi]
return (c1 == c2);
011916EE movsx eax,byte ptr [c1]
011916F2 movsx ecx,byte ptr [c2]
011916F6 cmp eax,ecx
011916F8 jne Equal+36h (01191706h)
011916FA mov dword ptr [ebp-0C4h],1
01191704 jmp Equal+40h (01191710h)
01191706 mov dword ptr [ebp-0C4h],0
01191710 mov eax,dword ptr [ebp-0C4h]
}
01191716 pop edi
01191717 pop esi
01191718 pop ebx
01191719 mov esp,ebp
0119171B pop ebp
0119171C ret

Саме ця адреса 0x011916D0 використовується при виконанні функції int


Equal(char c1, char c2). Так як покажчик також може зберігати адресу функції, то
функція може бути викликана не тільки за своїм ім’ям, а і за допомогою цього
покажчика. Він дозволяє також передавати її адресу іншим функціям у якості
аргумента.
У програмі на Сі адресою функції служить її ім'я без дужок і аргументів (це схоже
на адресу масиву, що являє собою ім’я масиву без індексів). Розглянемо наступну
програму, в якій порівнюються два рядки, введені користувачем. Звернемо увагу на
оголошення функції check() і покажчик p всередині main(). Покажчик p, як видно далі,
є покажчиком на функцію.

Приклад 16. Приклад застосування покажчика на функцію (для порівняння двох


рядків)

#include <stdio.h>
#include <string.h>

void check(char *a, char *b, int (*cmp)(const char*, const char*));

int main(void) {
char s1[81], s2[81];
int (*p)(const char*, const char*); /* покажчик на функцію */

p = strcmp; /* присвоює адресу функції strcmp вказівнику p */

printf("Введіть два рядки.\n");


gets(s1);
gets(s2);
- 110 -

check(s1, s2, p); / * Передає адресу функції strcmp


за допомогою покажчика p* /
return 0;
}

void check(char *a, char *b, int (*cmp)(const char *, const char *)) {
printf("Перевірка на збіг. \n");
if (!(*cmp)(a, b))
printf("Рівні");
else
printf("Не рівні");
}

Проаналізуємо цю програму детально. В першу чергу розглянемо оголошення


покажчика p в main():

int (*p) (const char*, const char*);

Це оголошення повідомляє компілятору, що p - це покажчик на функцію,яка має


два параметри типу const char* і повертає в своєму імені значення типу int. Дужки
навколо p необхідні для правильної інтерпретації оголошення компілятором.
Подібна форма оголошення використовується також для покажчиків на будь-які інші
функції, потрібно лише внести зміни в залежності від типу, що повертається і
параметрів функції.
Тепер розглянемо функцію check(). У ній оголошені три параметра: два
покажчика на символьний тип (a і b) і покажчик на функцію cmp. Слід звернути увагу
на те, що покажчик функції cmp оголошений точнісінько такого самого типу, що і
покажчик p у функції main(). Тому в cmp можна зберігати значення покажчика на
функцію, що має два параметри типу const char * і повертає значення int. Як і в
оголошенні p, круглі дужки навколо *cmp необхідні для правильної інтерпретації
цього оголошення компілятором.
На початку програми покажчику p присвоюється адреса стандартної
бібліотечної функції strcmp(), яка порівнює рядки. Потім програма просить
користувача ввести два рядки і передає покажчики на них функції check(), яка їх
порівнює. Усередині check() умовний вираз в if

(*сmp) (a, b)

викликає функцію strcmp(), на яку вказує cmp, з аргументами a і b. Дужки навколо


*cmp необов'язкові. Існує й інший, більш простий, спосіб виклику функції за
допомогою покажчика:

cmp (a, b);

Однак перший спосіб використовується частіше (і рекомендується використовувати


саме його), тому що при другому способі виклику покажчик cmp дуже схожий на ім'я
функції, що може збити з пантелику того, хто читає програму. У той же час у першого
способу запису є свої переваги, наприклад, добре видно, що функція викликається
за допомогою покажчика на функцію, а не імені функції. Слід зазначити, що спочатку
в Сі був визначений саме перший спосіб виклику.
- 111 -
При такому оголошенні функції check() її виклик можна записати,
використовуючи безпосередньо ім'я strcmp():

check (s1, s2, strcmp);

В цьому випадку вводити в програму додатковий покажчик p немає необхідності.


Може виникнути питання: яка користь від виклику функції за допомогою
покажчика на функцію? Адже в даному випадку ніяких переваг не досягнуто, цим ми
тільки ускладнили розуміння програми. Тим не менш, у багатьох випадках
виявляється більш вигідним передати ім'я функції як параметр або навіть створити
масив функцій. Наприклад, в програмі компілятора синтаксичний аналізатор
(програма, що аналізує вирази) часто викликає різні допоміжні функції, такі як
обчислення математичних функцій, процедури введення-виведення і т.і. У таких
випадках найчастіше створюють масив покажчиків на функції і викликають їх за
допомогою індексу.
Альтернативний підхід – використання оператора switch з довгим списком міток
case – робить програму більш громіздкою і схильною до помилок.
У наступному прикладі розглядається розширена версія попередньої програми.
У цій версії функція check() влаштована так, що може виконувати різні операції над
рядками s1 і s2 (наприклад, порівнювати кожен символ з відповідним символом
іншого рядка або порівнювати числа, записані в рядках) в залежності від того, яка
функція вказана в списку аргументів . Наприклад, рядки "0123" і "123" відрізняються,
проте представляють одне і те ж числове значення.

Приклад 7.17 Дороблений варіант порівняння програми двох вхідних значень

#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>

void check(char *a, char *b,


int (*cmp)(const char*, const char*));
int compvalues(const char *a, const char *b);

int main(void) {
char s1[80], s2[80];

printf("Введіть два значення або два рядки.\n");


gets(s1);
gets(s2);

if (isdigit(*s1)) {
printf("Перевірка числових значень на рівність.\n");
check(s1, s2, compvalues);
}
else {
printf("Перевірка рядків на рівність.\n");
check(s1, s2, strcmp);
}

return 0;
- 112 -
}

void check(char *a, char *b, int (*cmp)(const char*, const char*)) {
if (!(*cmp)(a, b))
printf("Рівні");
else
printf("Не рівні");
}

int compvalues(const char *a, const char *b) {


if (atoi(a)==atoi(b))
return 0;
else
return 1;
}

Якщо в цьому прикладі ввести перший символ першого рядка як цифру, то check()
використовує для порівняння функцію compvalues(), в іншому випадку - strcmp().
Функція check() викликає ту функцію, ім'я якої зазначено в списку аргументів при
виклику check(), тому вона в різних ситуаціях може викликати різні функції. Нижче
наведені результати роботи цієї програми в двох випадках:

Введіть два значення або два рядки.


тест
тест
Перевірка рядків на рівність.
Рівні

Введіть два значення або два рядки.


0123
123
Перевірка значень на рівність.
Рівні
Порівняння рядків "0123" і "123" показує рівність їх значень.

7.11. Рекурсивні функції.


Рекурсивним називається спосіб побудови об'єкта (поняття, системи, опис дії),
в якому визначення об'єкта включає аналогічні об'єкти (поняття, систему, дію) у
вигляді складових частин. Приклади рекурсії в житті:

«У попа була собака, він її любив.


Вона з'їла шматок м'яса, він її убив.
Каменем придавив, і на камені написав:
«У попа була собака ...» »(Дитяча лічилка)

Я хочу Вам написати, що я хочу Вам написати, що я хочу Вам написати. . .


(З листа пацієнта психіатру)

«Я знаю, що ти знаєш, що я знаю» (кінокомедія Альберто Сордо, Італія, 1982).

«Я оглянулся посмотреть,
не оглянулась ли она,
- 113 -
чтоб посмотреть, не оглянулся ли я...»
(Максим .Леонидов, «Девочка-видение», песня).

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


системи відтворює саму себе. Тут же зазначається особливість такого підходу:
рекурсія не властива буденному сприйняттю, безумовна рекурсія нескінченна і
безглузда. Обмежена рекурсія з певною кількістю рівнів - парадоксальна і
небезпечна. Наприклад, рефлексія (від лат. Reflexio - звернення назад) -
самопізнання, самоспостереження, образне уявлення самого себе в третій особі
(читати, наприклад, «Поєдинок» О. Купріна).
Приклади рекурсії можна виявити в математиці. Наприклад, рекурентні
співвідношення визначають (обчислюють) певний елемент послідовності через
кілька попередніх (числа Фібоначчі - задача розмноження кроликів, факторіал).
У програмуванні таких прикладів ще більше:
- визначення будь-якого конкретного оператора (умовний, цикл, блок) в якості
складових частин включає довільний оператор;
- рекурсивна структура даних - елемент структури даних містить один або кілька
покажчиків на аналогічну структуру даних. Наприклад, багатовимірний масив можна
визначити як лінійний масив покажчиків на багатовимірні масиви на одиницю
меншої розмірності;
- рекурсивна функція - тіло функції містить прямий чи опосередкований (через
іншу функцію) власний виклик.
Таким чином, рекурсія - це така організація виконання роботи функції, при якій
дана функція викликає сама себе.
Рекурсія може бути прямою і не прямою, а також лінійною і розгалуженою.
У разі прямої рекурсії виклик функцією самої себе робиться безпосередньо в цій
же функції:
void f() {….
f();
..}
Непряма рекурсія створюється за рахунок виклику даної функції з будь-якої іншої
функції, яка сама викликалася з даної функції. Наприклад, схема може бути такою:

Рис. 7.1 Схема опосередкованого (непрямого) рекурсивного виклику функції А()

Як видно, в main() викликається функція A(), яка викликає B(), а та в свою чергу
викликає знову A(). Зрозуміло, що ланцюжок функцій між викликами функції A()
може складатися не з однієї функції B(), а з більшої кількості різних функції.
Обидва вищевказаних приклади і літературно-художні приклади відносилися до
типу лінійної рекурсії. Разом з лінійної рекурсією, коли визначення об'єкта включає
в себе єдиний аналогічний об'єкт, існує ще й розгалужена (множинна) рекурсія,
коли таких об'єктів, що включаються, є декілька. Для рекурсивних функцій це
виглядає як два і більше окремих виклики функцією самої себе, або як рекурсивний
виклик функції в циклі.

/* Рекурсія, що розгалужується */
- 114 -
void F1(){
…if (…) F1(); /* Не більше 2 рекурсивних викликів */
…if (…) F1();
}

Для рекурсії, що розгалужується, є тільки одна фізична аналогія - ланцюгова


реакція: кожен рекурсивний виклик породжує n собі подібних. У структурах даних
аналогічна «конструкція» називається деревом.
Стандартом мови C++ рекурсивно може викликатися будь-яка функція, крім
main(), а за стандартом мови програмування Сі рекурсивно можна викликатися
навіть main()!
Очевидно, що потужність рекурсії пов'язана з тим, що вона дозволяє визначити
нескінченні обчислення за допомогою кінцевої рекурсивної програми, навіть якщо
ця програма не містить явних циклів. Однак найкраще використовувати рекурсивні
алгоритми в тих випадках, коли вирішуване завдання, обчислювана функція, або
структура даних, що обробляється, самі визначені за допомогою рекурсії.
З функцією прийнято пов'язувати деякий перелік локальних об'єктів, тобто
змінних, констант, які визначені локально в цій функції, а поза нею не існують або
не мають сенсу. Кожен раз, коли така функція рекурсивно викликається, для неї
створюється у стеку нова множина локальних змінних. Хоча вони мають ті ж імена,
що і відповідні елементи множини локальних змінних, створеної при попередньому
зверненні до цієї ж функції, їх значення та розміщення в стеку різні. Правила області
дії ідентифікаторів дозволяють виключити будь-який конфлікт при використанні
імен: ідентифікатори завжди посилаються на множину змінних, створену останньою.
Те ж правило відноситься і до параметрів функції.
Подібно операторам циклу, рекурсивні функції можуть привести до нескінченних
обчислень. Тому необхідно розглянути проблему закінчення роботи функції.
Очевидно, що для того, щоб робота колись завершилася, необхідно, щоб
рекурсивне звернення до функції підпорядковувалося умові, яка в якийсь момент
перестає виконуватися. Іншими словами, не залежно від того, який вид рекурсії
використовується (пряма або непряма), у рекурсивної функції обов'язково повинна
виконуватися перевірка якоїсь умови, в залежності від якої здійснювався б вихід з
рекурсивної функції. Причому, необхідно гарантувати, що ця умова обов'язково
буде виконана через скінченне число рекурсивних викликів, і функція врешті-решт
закінчить роботу. Інакше – аварійне завершення програми.
Послідовний виклик функцією самої себе називають рекурсивним спуском,
послідовний вихід з багаторазового виклику - рекурсивним підйомом.
На практиці, перед використанням рекурсії, потрібно обов'язково переконатися,
що найбільша глибина рекурсії не тільки кінцева, але і достатньо мала. Справа в
тому, що при кожному рекурсивному виклику функції Р, у програмному стеку
виділяється деяка пам'ять для розміщення її змінних. Крім цих локальних змінних
потрібно ще зберігати поточний стан обчислень, щоб повернутися до нього, коли
закінчиться виконання нової активації Р і потрібно буде повернутися до старої.
Приклад прямо-рекурсивної і не рекурсивної функції обчислення факторіала:

/ * Прямо-рекурсивна функція обчислення факторіала * /


int factr(int n) {
int answer;
if (n==1)
return (1);
answer = factr(n-1)*n; /* рекурсивний виклик*/
return(answer);
- 115 -
}

/* не рекурсивна функція обчислення факторіала */


int fact(int n) {
int t, answer;
answer = 1;
for(t=1; t<=n; t++)
answer=answer*(t);
return(answer);}

7.11. Тип посилання (С++)


Тип посилання, іноді званий псевдонімом, служить для задання довільному
програмному об'єкту додаткового імені. Посилання дозволяє опосередковано
маніпулювати об'єктом, так саме, як це робиться за допомогою покажчика. Однак
ця непряма маніпуляція не вимагає використання спеціального синтаксису,
необхідного для покажчиків. Найчастіше посилання вживаються як формальні
параметри функцій. Розглянемо самостійне використання об'єктів типу посилання.
Тип посилання позначається позначкою оператора взяття адреси & перед ім'ям
змінної при її оголошенні. При цьому посилання має бути обов'язково
проініціалізоване. Наприклад:

int ival = 1024;


int &refVal = ival; // правильно: refVal - посилання на ival
int &refVal2; /* помилка: посилання повинно бути зразу проініціалізовано! */

Хоча, як вже казали, посилання дуже схоже на покажчик, воно повинно бути
ініціалізоване не адресою об'єкта, а його значенням. Таким об'єктом, для якого
використовується посилання, може бути і покажчик:

int ival = 1024;


int &refVal = &ival; // помилка: refVal має тип int, а не int*!
int* pi = &ival;
int* &ptrVal2 = pi; /* правильно: ptrVal2 - посилання на покажчик */

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


неможливо змінити так, щоб працювати з іншим об'єктом (саме тому посилання
повинно бути ініціалізоване в місці свого оголошення (визначення)). У наступному
прикладі оператор присвоювання не змінює значення refVal з попереднього
прикладу, нове значення присвоюється змінній ival - тій, на яку посилається refVal.

int ival = 1024;


int &refVal = ival;
int min_val = 0;
refVal = min_val; /* ival отримує значення min_val,
а не refVal змінює своє значення на min_val! */

Всі операції з посиланням реально впливають не на саму змінну посилання, а


на адресований нею об'єкт. У тому числі і операція взяття адреси. Наприклад:

refVal += 2;

прибавляє 2 до ival – змінної, на яку посилається refVal. Аналогічно


- 116 -

int ii = refVal;

присвоює ii поточне значення ival,

int* pi = &refVal;

ініціалізує pi адресою ival.


Якщо ми оголошуємо декілька посилань в одному операторові через кому,
перед кожним об'єктом типу посилання обов'язково повинен стояти амперсанд & -
оператор взяття адреси (точно так само, як і для покажчиків - зірочка). Наприклад:

int ival = 1024, ival2 = 2048; /* визначено два об'єкти типу int */
int &rval = ival, rval2 = ival2; /* визначене одна посилання і один об'єкт */
int ival3 = 1024, *pi = &ival3, &ri = ival3; /* визначено один об'єкт, один
покажчик на цей об'єкт і
одне посилання на цей же об'єкт*/
int &rval3 = ival3, &rval4 = ival2; /* визначені два посилання */

Константне посилання може бути ініціалізоване об'єктом іншого типу (якщо,


звичайно, існує можливість перетворення одного типу в інший), а також
безадресною величиною - такою, як літеральна константа. Наприклад:

double dval = 3.14159;

/* Наступні три визначення вірні тільки для константних посилань! */

const int &ir = 1024;


const int &ir2 = dval;
const double &dr = dval + 1.0;

Якби ми не вказали специфікатор const, всі три визначення посилань викликали


б помилку компіляції. Однак, причина, по якій компілятор не пропускає такі
визначення, залишається невідома. Спробуємо розібратися. Для констант у правій
частині ініціалізатора це більш-менш зрозуміло: у нас не повинно бути можливості
мимохідь змінювати значення константи у програмі, використовуючи покажчики на
неї, або посилання. Що ж стосується об'єктів іншого типу, то слід зауважити, що
компілятор перетворює початковий об'єкт в певний допоміжний проміжний.
Наприклад, якщо ми пишемо:

double dval = 1024;


const int &ri = dval;

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

int temp = dval;


const int &ri = temp;

Якби ми могли присвоїти нове значення посиланню ri, ми б реально змінили не


dval, а temp. Значення dval залишилося б тим же, що абсолютно неочевидно для
програміста. Тому компілятор забороняє такі дії, і єдина можливість
проініціалізувати посилання об'єктом іншого типу – це оголосити його як const.
- 117 -
Ось ще один приклад посилання, який важко зрозуміти з першого разу. Ми
хочемо визначити посилання на адресу константного об'єкта, але наш перший
варіант зразу викликає помилку компіляції:

const int ival = 1024;


int* &pi_ref = &ival; /* помилка: потрібне константне посилання! */

Спроба виправити справу додаванням специфікатора const теж не проходить:

const int* &pi_ref = &ival; /* все одно помилка! */

У чому ж причина? Уважно прочитавши визначення, ми побачимо, що pi_ref є


посиланням на константний покажчик на константний об'єкт типу int. А нам потрібен
неконстантний покажчик на константний об'єкт, тому правильним буде такий запис:
const int ival = 1024;
int* const &piref = &ival; /* правильно! */

Між посиланням і покажчиком існують дві основні відмінності. По-перше,


посилання обов'язково повинне бути ініціалізоване в місці свого визначення. По-
друге, будь-яка зміна посилання перетворює не його, а той об'єкт, на який він
посилається. Розглянемо на прикладах. Якщо ми пишемо:

int *pi = 0;

то ми ініціалізуємо покажчик pi нульовим значенням, а це значить, що pi не вказує


ні на який об'єкт. У той же час запис

const int &ri = 0;

означає приблизно наступне:

int temp = 0;
const int &ri = temp;

Що стосується операції присвоєння, то в наступному прикладі:

int ival = 1024, ival2 = 2048;


int *pi = &ival, *pi2 = &ival2;
pi = pi2;

змінна ival, на яку вказує pi, залишається незмінною, а pi отримує значення адреси
змінної ival2. І pi, і pi2 і тепер вказують на один і той же об'єкт ival2. Якщо ж ми
працюємо з посиланнями:

int &ri = ival, &ri2 = ival2;


ri = ri2;

то саме значення ival змінюється, але посилання ri, як і раніше, адресує ival.
- 118 -

Приклад використання посилань

У реальних C/С++ програмах посилання рідко використовуються як


самостійні об'єкти, зазвичай вони вживаються в якості формальних параметрів
функцій. За допомогою типу посилання ми забезпечуємо ще один механізм
передачі результатів роботи функції у програму, що її викликала. Приклад
використання механізму посилань наведено нижче для функції, яка міняє значення
двох змінних місцями:

void swap(int &x, int &y) {


x = x + y; /* x=a+b, y=b */
y = x - y; /* x=a+b, y=a */
x = x - y; /* x=b, y=a */
}

Виклик функції swap() з програми здійснюється так:


swap (I,j);
Як можна легко переконатися, подібний підхід набагато легше використання
механізму покажчиків, як раніше:

void swap(int *x, int *y) {


*x = *x + *y; /* x=a+b, y=b */
*y = *x - *y; /* x=a+b, y=a */
*x = *x - *y; /* x=b, y=a */
}

І виклик
swap(&I,&j);
відповідно.
- 119 -

ТЕМА 8. КОРИСТУВАЦЬКІ ТИПИ ДАНИХ

У мові Сі є п'ять способів створення користувацьких типів даних. Користувацькі


типи даних можна створити за допомогою:
 структури - групи змінних, що має одне ім'я і зветься агрегатним типом
даних;
 об'єднання, яке дозволяє визначати одночасно одну й ту саму ділянку
пам'яті для двох або більше змінних різних типів;
 бітового поля, яке є спеціальним типом елемента структури або
об'єднання, що дозволяє легко отримувати доступ до окремих бітів членів
такого агрегатного типу;
 нумератора (порядкового типу) - списку пойменованих цілих констант;
 ключового слова typedef, яке визначає нове ім'я для вже існуючого
типу даних.
8.1. Структури
Структура - це сукупність змінних, об'єднаних під одним ім'ям. За допомогою
структур зручно розміщувати в суміжних полях пов'язані між собою елементи
інформації. Оголошення структури створює шаблон типу, який можна
використовувати для створення об'єктів такого типу. Змінні, з яких складається
структура, називаються членами (елементами) або полями структури.
Як правило, члени структури пов'язані між собою за змістом. Наприклад,
елемент списку розсилки, що складається з імені та адреси, логічно буде
представити у вигляді структури. У наступному фрагменті коду показано, як
оголосити структуру, в якій визначені поля імені і адреси. Ключове слово struct
повідомляє компілятору, що оголошується (кажуть, декларується) структура.

struct addr {
char name[30];
char street[40];
char city[20];
unsigned int zip;
};

Оголошення обов'язково завершується крапкою з комою, тому що оголошення


структури є оператором. Крім того, тег структури addr ідентифікує цю конкретну
структуру даних і є специфікатором її типу.
В даному випадку, насправді, ніяка змінна ще не створюється, пам'ять під неї не
відводиться. Всього лише визначається вид даних (тобто, шаблон) у формі
агрегатного типу, а не сама змінна! І поки не буде оголошена змінна цього типу, то
існувати вона не буде. Щоб оголосити змінну (тобто фізичний об'єкт) типу addr, слід
вказати у розділі оголошень блока (функції) оператор:

struct addr addr_info;

У цьому операторі оголошена змінна типу addr, на ім'я addr_info. Таким чином, addr
описує вид структури (її тип), a addr_info є екземпляром (об'єктом) цієї структури.
Коли оголошується змінна-структура, компілятор автоматично виділяє
необхідну кількість пам'яті, достатню, щоб розмістити всі її члени. На Рис. 8.1 нижче
показано, як addr_info розміщена в пам'яті; в даному випадку мається на увазі, що
цілі змінні типу int займають по 4 байти.
- 120 -

+------------------------------------------+
|Name (ім’я) 30 байт |
+------------------------------------------+

+-------------------------------------------------+
|Street (вулиця) 40 байт |
+-------------------------------------------------+

+-----------------------------------+
|City (місто) 20 байт |
+-----------------------------------+

+----------------------------+
|Zip (код) 4 байта |
+----------------------------+
Рис. 8.1. Розташування в пам’яті структури addr_info

Одночасно з оголошенням типу структури можна зробити і оголошення одної


або кількох змінних. Наприклад,

struct addr {
char name[30];
char street[40];
char city[20];
unsigned int zip;
} addr_info, binfo, cinfo;

визначає тип структури на ім'я addr і зразу оголошує змінні цього типу addr_info, binfo
і cinfo. Важливо розуміти, що кожна змінна-структура містить власні копії членів
структури. Наприклад, поле zip в binfo відрізняється від поля zip в cinfo. Зміни в zip
з binfo не вплинуть на вміст поля zip, що знаходиться в cinfo.
Якщо потрібна тільки одна змінна-структура, то тег для такої структури є зайвим.
У цьому випадку наш приклад оголошення можна переписати таким чином:

struct {
char name[30];
char street[40];
char city[20];
unsigned long int zip;
} addr_info;

В цьому випадку оголошується одна змінна з ім'ям addr_info, причому її поля


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

struct тег {
тип ім’я-члена;
тип ім’я-члена;
тип ім’я-члена;
.
.
.
- 121 -
} змінні-структури;

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

Доступ до членів структури

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


(Який зазвичай називають оператором крапка або оператором доступу до члена
структури). Наприклад, в наступному виразі полю zip в уже оголошеній змінній-
структурі addr_info присвоюється значення ZIP-коду, що дорівнює 02091:

addr_info.zip = 02091;

Цей окремий член визначається ім'ям об'єкта структури (в даному випадку


addr_info), за яким слідує крапка, а потім ім'я самого цього члена структури (в
даному випадку zip). У загальному вигляді використання оператора крапка для
доступу до члена структури виглядає таким чином:

ім’я-об’єкту.ім’я-члена

Тому, щоб вивести ZIP-код на екран, пишемо наступний оператор:

printf ("%d", addr_info.zip);

Буде виведений ZIP-код, який знаходиться в полі zip змінної-структури addr_infо.


Точно так у виклику gets() можна використовувати масив символів
addr_infо.name:

gets(addr_info.name);

Таким чином, функції gets() передається покажчик на символьний рядок


addr_info.name.
Так як addr_info.name є масивом символів, то щоб отримати доступ до окремих
символів в цьому масиві, можна використовувати індекси разом з addr_info.name.
Наприклад, за допомогою наступного коду можна посимвольно вивести на екран
вміст addr_info.name:

for (t=0; addr_info.name[t]; ++t)


putchar(addr_info.name[t]);

Тут індексується саме addr_info.name (а не addr_info!). Слід весь час пам'ятати, що


addr_info - це ім'я всього об'єкта-структури, a name - ім'я елемента цієї структури.
Таким чином, якщо потрібно індексувати елемент структури, то індекс необхідно
вказувати після імені цього елемента.

Присвоювання структур

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


структурі того ж самого типу за допомогою єдиного оператора присвоювання. Немає
необхідності присвоювати значення кожного члена окремо. Як виконується
присвоювання структур, показує наступна програма:
- 122 -

Приклад 8.01. Присвоєння значення однієї структури іншій

#include <stdio.h>
int main(void) {
struct {
int a;
int b;
} x, y;

x.a = 10;

y = x; /* присвоювання значень однієї структури іншій*/

printf("%d", y.a);

return 0;
}

Після виконання присвоєння в y.a буде зберігатися значення 10.

8.2. Масиви структур


Структури часто утворюють масиви. Щоб оголосити масив структур, спочатку
необхідно визначити структуру (тобто визначити агрегатний тип даних), а потім
оголосити змінну масиву цього ж типу. Наприклад, щоб оголосити 100-елементний
масив структур типу addr, який був визначений раніше, пишемо наступне:

struct addr addr_list[100];

Цей оператор створить масив зі 100 екземплярів структур, кожен з яких


організований так, як визначено при опису типу структури addr.
Щоб отримати доступ до певної структури, вказується ім'я масиву з індексом.
Наприклад, щоб вивести ZIP-код з третьої структури масиву, напишемо:

printf("%d", addr_list[2].zip);

Як і в інших масивах, в масивах структур індексування починається з 0.


Для довідки: щоб вказати певну структуру, що знаходиться в масиві структур,
необхідно вказати ім'я цього масиву з певним індексом. А якщо потрібно вказати
індекс певного елемента в цьому члені структури, то необхідно вказати ще й індекс
цього елемента після імені члена. Таким чином, в результаті виконання наступного
оператора найпершому символу члена name, що знаходиться в третій структурі з
addr_list, присвоюється символ 'X':

addr_list[2].name[0] = 'X';

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


приклад простої програми роботи зі списком розсилки: в її масиві структур будуть
зберігатися адреси і пов'язана з ними інформація. Ця інформація записується в
наступні поля: name (ім'я), street (вулиця), city (місто) і zip (поштовий індекс).

Приклад 8.02. Програма роботи зі списком розсилки


- 123 -

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

struct addr {
char name[30];
char street[40];
char city[20];
unsigned int zip;
} addr_list[MAX];

Ось main() — перша функція, яка потрібна програмі:

int main(void) {
char choice;

SetConsoleCP(1251);
SetConsoleOutputCP(1251);

init_list(); /* ініціалізація масиву структур */

for (;;) {
choice = menu_select();
switch (choice) {
case '1':
if (enter())
puts("\nМасив розсилки заповнено. Нового клієнта не
додано!\n");
else
puts("\nНового клієнта додано!\n");
break;
case '2':
if (deleteInd())
puts("\nДаний індекс клієнта не існує. Клієнта не
видалено!\n");
else
puts("\nКлієнта видалено!\n");
break;
case '3': list();
break;
case '4': exit(0);
}
}
return 0;
}
Функція починає виконання з ініціалізації масиву структур, а потім реагує на обраний
користувачем пункт меню.
Функція init_list() готує масив структур до використання, обнуляючи перший байт
поля name кожної структури масиву. (У програмі передбачається, що якщо поле
name пусте, то елемент масиву не використовується.) А ось сама функція init_list ():

/* Ініціалізація списку. */
void init_list(void) {
- 124 -
for (register int t = 0; t<MAX; ++t)
addr_list[t].name[0] = '\0';
return;
}

Функція menu_select() виводить меню на екран і повертає те, що вибрав


користувач.

/* Робота меню. */
int menu_select(void) {
char s;
int c;

printf("1. Введіть нового клієнта\n");


printf("2. Видаліть клієнта за індексом\n");
printf("3. Виведіть список\n");
printf("4. Вихід\n");

do {
printf("\nВведіть номер потрібного пункту: ");
s = _getche();
} while (s<'1' || s>'4');

return s;
}

Функція enter() підказує користувачеві, що саме потрібно ввести, і зберігає


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

/*Додавання адреси до списку. */


int enter(void) {
int slot;
char s[81];

slot = find_free();
if (slot == -1) {
printf("\nСписок заповнений");
return -1;
}

printf("\nВведіть ім’я: ");


fgets(addr_list[slot].name, 28, stdin);

printf("\nВведіть вулицю: ");


fgets(addr_list[slot].street, 38, stdin);

printf("\nВведіть місто: ");


fgets(addr_list[slot].city, 18, stdin);

printf("\nВведіть поштовий індекс: ");


fgets (s, 79, stdin);
addr_list[slot].zip = atoi(s); /* Функція перетворює строкове
- 125 -

представлення числа, яке міститься в


рядку
s, в значення типу unsigned int і

повертає отриманий результат. */


return 0;
}

Функція find_free() шукає в масиві структур перший вільний елемент.

/* Функція find_free() шукає в масиві структур перший вільний елемент */


int find_free(void) {
register int t;
for (t=0; addr_list[t].name[0] && t<MAX; ++t) ;
if (t==MAX) return -1; /* вільних структур нема */
return t;
}

Якщо всі елементи масиву структур зайняті, то find_free() повертає -1. Це зручне
число, тому що в масиві немає -1-го елемента.
Функція delete() пропонує користувачеві вказати індекс того запису з адресою,
який потрібно видалити. Потім функція для видалення лише обнуляє перший байт
поля name.

/* Видалення адреси */
int deleteInd (void) {
register int slot;
char s[81];

puts ("\nВведіть № клієнта: ");


fgets(s, 79, stdin);
slot = atoi(s);

if (slot >= 0 && slot < MAX && addr_list[slot].name[0]) {


addr_list[slot].name[0] = '\0';
return 0;
}
else
return -1;
}

І остання функція, яка потрібна програмі, – це list(), яка виводить на екран весь
список розсилки:

/*Виведення списку на екран */


void list(void) {
puts("\n\n");
for (register int t = 0; t<MAX; ++t) {
if (addr_list[t].name[0]) {
printf("%s\n", addr_list[t].name);
printf("%s\n", addr_list[t].street);
- 126 -
printf("%s\n", addr_list[t].city);
printf("%lu\n\n", addr_list[t].zip);
}
}
printf("\n\n");
return;
}

8.3. Передача структур до функції

Передача членів структур до функції

При передачі члена структури до функції передається його значення, при цьому
не грає ролі те, що значення береться з члена структури. Приклад – наступна
структура і наступні передачі її членів у функцію:

struct fred {
char x;
int y;
float z;
char s[10];
} mike;

Наприклад, яким чином кожен член цієї структури передається функціям:

func(mike.x); /* передається символьне значення x */


func2(mike.y); /* передається ціле значення y */
func3(mike.z); /* передається значення з плаваючою точкою z */
func4(mike.s); /* передається адреса рядка s */
func(mike.s[2]); /* передається символьне значення s[2] */

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


елемента структури, і тут не має значення те, що цей елемент є частиною чогось
більшого.
Якщо ж потрібно передати адресу окремого члена структури, то перед ім'ям
структури повинен знаходитися оператор &. Наприклад, щоб передати адреси
членів структури mike, можна написати так:

func(&mike.x); /* передається адреса символа x */


func2(&mike.y); /* передається адреса цілого y */
func3(&mike.z); /* передається адреса члена z з плаваючою точкою*/
func4(mike.s); /* передається адреса рядка s */
func(&mike.s[2]); /* передається адреса символа в s[2] */

Оператор & стоїть безпосередньо перед ім'ям структури, а не перед ім'ям окремого
члена! І ще, оскільки s вже позначає адресу (найпершого елемента масиву
символів), оператор & не потрібен.

Передача цілої структури функції

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


цілої структури використовується звичайний спосіб виклику за значенням. Це,
- 127 -
означає, що будь-які зміни у вмісті параметра-структури всередині функції не
позначаться на значеннях членів тієї структури, яка передається у якості аргументу.
При використанні структури як аргументу, треба пам'ятати, що тип аргументу
повинен точно відповідати типу параметра функції. Наприклад, в наступній програмі
і аргумент arg, і параметр parm оголошуються з одним і тим же типом структури:

#include <stdio.h>

/ * Визначення типу структури. * /


struct struct_type {
int a, b;
char ch;
};

void f1(struct struct_type parm);

int main(void) {
struct struct_type arg;
arg.a = 1000;
f1(arg);
return 0;
}

void f1(struct struct_type parm) {


printf("%d", parm.a);
}

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


оголошення типу структури має бути глобальним, щоб структурний тип можна було
використовувати у всій програмі. Тобто, якби struct_type був би оголошений
всередині main(), то цей тип не було б видно в f1().
При передачі структури тип аргументу повинен точно збігатися з типом
параметра. Для аргументу і параметра недостатньо просто бути фізично схожими;
повинні збігатися навіть імена їх типів. Наприклад, наступна версія попередньої
програми неправильна і компілюватися не буде. Справа в тому, що ім'я типу для
аргументу, використовуваного при виконанні функції f1(), відрізняється від імені типу
її параметра.

Приклад 8.03. Ця програма неправильна і при компіляції будуть виявлені


помилки.

#include <stdio.h>

/* Визначення типу структур. */


struct struct_type {
int a, b;
char ch;
};

/ * Визначення структури, схожої на struct_type,


але з іншими іменами. * /
struct struct_type2 {
- 128 -
int a, b;
char ch;
};

void f1(struct struct_type2 parm);

int main(void) {
struct struct_type arg;
arg.a = 1000;
f1(arg); /* неспівпадіння типів */
return 0;
}

void f1(struct struct_type2 parm) {


printf("%d", parm.a);
}

8.4. Покажчики на структури


Як і інші покажчики, покажчик на структуру оголошується за допомогою зірочки
*, яку розміщують перед ім'ям змінної структури. Наприклад, для раніше визначеної
структури addr такий вираз оголошує addr_pointer покажчиком на дані цього типу
(тобто на дані типу addr):

struct addr* addr_pointer;

Покажчики на структури використовуються головним чином в двох випадках:


коли структура передається функції за допомогою виклику за посиланням, і коли
створюються пов'язані один з одним списки та інші структури з динамічними даними,
що працюють на основі динамічного розміщення (абстрактні типи даних).
У такого способу, як передача за значенням (тобто, копій) будь-яких (крім
найпростіших) структур функції, є один великий недолік: при виконанні виклику
функції, щоб помістити структуру в стек, необхідні істотні ресурси (адже всі
аргументи зі списку параметрів передаються функції виключно через стек!). Втім,
для простих структур з декількома членами ці ресурси є не такими вже й великими.
Але якщо в структурі є велика кількість членів або деякі члени самі є масивами, то
при передачі структури функції продуктивність роботи програми може впасти до
неприпустимо низького рівня. Ця проблема вирішується шляхом передачі не самої
структури, а покажчика на неї. Тобто, використанням способу передачі параметру
за посиланням.
Коли функції передається покажчик на структуру, то в стек потрапляє тільки
адреса структури. В результаті виклики функції виконуються дуже швидко. У деяких
випадках цей спосіб має ще й другу перевагу: передача покажчика дозволяє функції
модифікувати вміст структури, що використовується в якості аргументу.
Щоб отримати адресу змінної-структури, необхідно перед її ім'ям помістити
оператор &. Наприклад, в наступному фрагменті коду

struct bal {
float balance;
char name[80];
} person;

struct bal *p; /* оголошення покажчика на структуру */


- 129 -

адресу структури person можна присвоїти покажчику p:

p = &person;

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


необхідно використовувати замість оператора "." оператор «стрілка: «->»». Ось,
наприклад, як можна послатися на поле balance за допомогою покажчика на
структуру p:

p->balance

Оператор ->, який зазвичай називають оператором стрілки, складається зі знака


"мінус", за яким слідує знак "більше". Стрілка застосовується замість оператора
крапки тоді, коли для доступа до члена структури використовується покажчик на
структуру.
Слід пам'ятати, що оператор крапка використовується для доступа до
елементів структури при прямій роботі з самою структурою (тобто, за іменем
структури). А коли використовується покажчик на структуру, то треба застосовувати
оператор стрілка.
8.5. Масиви і вкладені структури всередині структур
Членом структури може бути або проста змінна, наприклад, типу int або double,
або складений (не скалярний) тип. У мові Сі складеними типами є масиви і
структури. Один складений тип ми використовували в прикладі зі структурою addr -
це символьні масиви.
Члени структури, які є масивами, можна вважати такими ж членами структури.
Наприклад, проаналізуємо таку структуру:

struct x {
int a[10][10]; /* масив 10 x 10 цілих значень */
float b;
} y;

Тут доступ до елементу з індексами [3, 7] з масиву a, що знаходиться в структурі y,


задається таким чином:

y.a[3][7]

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


структурою. Наприклад, в наступному прикладі структура address вкладена в
структуру emp:

struct emp {
struct addr address; /* вкладена структура */
float wage;
} worker;

Тут структура була визначена як така, що має два члена. Першим членом є
структура типу addr, в якій знаходиться адреса працівника. Другий член - це wage,
де знаходяться дані про його зарплату. У наступному фрагменті коду елементу zip
з address присвоюється значення 93456.
- 130 -

worker.address.zip = 93456;

Відповідно до стандарту С89 структури можуть бути вкладеними аж до 15-го рівня.


А стандарт С99 допускає рівень вкладеності до 63-ого включно.
8.6. Об’єднання
Об'єднання – являє собою значення або структуру даних, яке може мати кілька
представлень різного типу під різними іменами. Об'єднання дає можливість
інтерпретувати один і той же набір бітів не менше, ніж двома різними способами.
Оголошення об'єднання (починається з ключового слова union) схоже на
оголошення структури і загалом виглядає так:

union тег {
тип ім’я-члена;
тип ім’я-члена;
тип ім’я-члена;
.
.
.
} змінні-цього-об’єднання;

Наприклад:

union pw {
short int i;
char ch[2];
};

Нагадуємо, що це оголошення ще не створює ніяких змінних. Щоб оголосити змінну,


її ім'я потрібно помістити в кінці оголошення або написати окремий оператор
оголошення. Щоб за допомогою тільки що написаного коду оголосити змінну-
об'єднання, яка називається cnvt і має тип pw, потрібно написати наступний
оператор:

union pw cnvt;

В cnvt одну і ту ж область пам'яті займають коротка ціла змінна i та двобайтовий


символьний масив ch. Звичайно, i займає 2 байта (за умови, що цілі значення int
займають по 4 байти), a елемент масиву ch[j] - тільки 1. На Рис. 8.2 показано, яким
чином i та ch користуються одною і тою ж адресою. У будь-якому місці програми
дані, що зберігаються в cnvt можна обробляти як цілі або символьні.
|<--------- i ---------->|
| |
+----------+------------+
|Байт 0 | Байт 1 |
+----------+------------+
| | |
|<-ch[0]->|<-ch[1]->|
Рис. 8.2. Як члени об’єднання ch та i зберігаються в пам’яті (член об’єднання i має
тип short)
- 131 -
Коли змінна оголошується з ключовим словом union, компілятор автоматично
виділяє стільки пам'яті, щоб в ній зміг повністю розміститися найбільший член
нового об'єднання. Наприклад, за умови, що цілі короткі значення займають по 2
байта, для розміщення i в cnvt необхідно, щоб довжина цього об'єднання становила
2 байта, навіть якщо для ch потрібно тільки 1 байт.
Для отримання доступу до члена об'єднання використовуйте той же синтаксис,
що і для структур: операції крапки і стрілки. При роботі безпосередньо з
об'єднанням слід користуватися крапкою. А при отриманні доступу до об'єднання за
допомогою покажчика потрібна операція стрілка. Наприклад, щоб присвоїти ціле
значення 10 елементу i з cnvt, слід написати:

cnvt.i = 10;

У наступному прикладі функції func1 передається покажчик на cnvt:

void func1(union pw* un) {


un->i = 10; /* присвоєння cnvt значення 10 за допомогою покажчика */
}

Об'єднання часто використовуються тоді, коли потрібно виконати специфічне


перетворення типів, тому що дані, що зберігаються в об'єднанні, можна тлумачити
абсолютно різними способами. Наприклад, використовуючи об'єднання, можна
маніпулювати байтами, що складають значення типу double, і міняти його точність
або виконувати якесь незвичайне округлення.
Щоб отримати уявлення про корисність об'єднань у випадках, коли потрібні
нестандартні перетворення типу, розглянемо приклад запису цілих значень типу
short в файл, який знаходиться на диску.
Використовуючи об'єднання, можна легко створити функцію putw(), яка по
одному байту буде записувати в файл двійкове представлення цілого значення типу
short (змінна такого типу має довжину 2 байта). Щоб побачити, як це робиться, за
допомогою об'єднання pw можна написати варіант putw(), наведений в наступній
програмі:

Приклад 8.04. Запис у файл двійкового представлення цілого значення типу


short

#include <stdio.h>
#include <stdlib.h>

union pw {
short int i;
char ch[2];
};

int putW(short int num, FILE *fp);

int main(void) {
FILE *fp; /* вказівник на поток */

SetConsoleCP(1251);
SetConsoleOutputCP(1251);
- 132 -
fp = fopen("test.tmp", "wb+"); /* відкривається двійковий файл для читання і
запису */
if (!fp) {
printf("Файл не відкритий.\n");
exit(1);
}

putW(1025, fp); /* запис значення 1025 */


fclose(fp);
return 0;
}

int putW(short int num, FILE *fp) {


union pw word;

word.i = num;
putc(word.ch[0], fp); /* записати першу половину */
return putc(word.ch[1], fp); /* записати другу половину */
}

В свій час, на запит про необхідність об'єднань, розробник мови С++ Б'єрн
Страуструп відповів так: "Unions are used when you want to save space by reusing
space for information one type at a time. I don't use them much", що в перекладі звучить:
"Об'єднання використовуються, коли ви хочете заощадити пам'ять, використовуючи
одну і ту саму пам'ять для різної інформації. Я їх мало використовую". З цього може
випливати, що в наш час, коли ми міряємо оперативну пам'ять на гігабайти
необхідність використовувати об'єднання зникає. Однак практика засвідчує все нові
і нові використання об'єднань у сучасних програмах.
Об'єднання виявилося дуже корисною річчю при роботі з мікроконтролерами в
Інтернеті речей, коли необхідно обробляти дані, що надходять в "сирому" вигляді
(наприклад при передачі з давачів по сенсорній мережі).
Наприклад, при передачі по сенсорній мережі даних від одного давача можливі
два типа повідомлень:

struct tMsg1 {
char id;
unsigned int code1;
unsigned int code2;
};

struct tMsg2 {
char id;
char code_error;
};

Створюємо об'єднання:

union {
char data[maxLen];
tMsg1 msg1;
tMsg2 msg2;
} DATA;
- 133 -

Створюємо змінну

DATA data;

Запис одержуваних даних ведемо в змінну data.data, а інтерпретацію даних в


повідомленнях ведемо за допомогою значень data.msg1 або data.msg2 в залежності
від значення data.id. Звісно, тут можна обійтись і без об'єднання напряму
перетворюючи покажчик на "сирі" дані у покажчик на один з видів повідомлень та
далі працювати з полями повідомлень, але при включенні оптимізації в компіляторі
(наприклад -O2 або -O3) такий код перестає працювати. Для його працездатності
необхідно буде усюди при оголошенні змінної data не забувати додавати ключове
слово volatile.

8.7. Бітові поля


У мові Сі є вбудована підтримка бітових полів, яка дає можливість отримувати
доступ до одиничного біта у байтах. Бітові поля можуть бути корисними з різних
причин, а саме:
 якщо пам'ять обмежена, то в одному байті можна зберігати кілька булевих
змінних (які приймають значення ІСТИНИ і ХИБНОГО);
 деякі пристрої передають інформацію про стан, закодовану в байті в
одному або декількох бітах;
 для деяких процедур шифрування потрібен доступ до окремих бітів
всередині байта, тощо.
Хоча для вирішення цих завдань можна застосовувати побітові операції,
бітові поля можуть надати програмному коду більше впорядкованості і елегантності
(і з їх допомогою вдається досягти більшої ефективності).
Бітове поле має бути членом структури або об'єднання. Воно визначає
довжину поля в бітах. Загальний вигляд оголошення бітового поля такий:

тип ім'я: довжина;

Тут тип означає тип бітового поля, а довжина - кількість біт, які займає це поле. Тип
бітового поля може бути int, signed або unsigned. Ніяким іншим типом бітове поле
описано бути не може.
Бітові поля довжиною 1 біт повинні оголошуватися як unsigned, оскільки 1 біт
не може мати знака. Бітові поля можуть мати довжину від 1 до16 біт для 16-бітних
середовищ і від 1 до 32 біт для 32-бітних середовищ.
До кожного бітового поля відбувається звернення за допомогою оператора
"крапка". Проте, якщо звернення до структури (об'єднання) відбувається за
допомогою покажчика, то слід використовувати оператор ->.
Бітові поля часто використовуються при аналізі даних, що надходять в
програму з обладнання. Наприклад, в результаті опитування стану адаптера
послідовного зв'язку може повертатися байт стану, організований в такий спосіб:

Таблиця 8.1. Байт стану послідовного порта


- 134 -

Біт Що означає, якщо встановлений

0 Зміна в лінії сигналу дозволу на передачу (change in clear-to-send line)

1 Зміна стану готовності пристрою сполучення (change in data-set-ready)

2 Виявлений кінцевий запис (trailing edge detected)

3 Зміна в приймальній лінії (change in receive line)

Дозвіл на передачу. Сигналом CTS (clear-to-send) модем дозволяє підключеному


4
терміналу передавати дані

5 Модем готовий (data-set-ready)

6 Телефонний виклик (telephone ringing)

7 Сигнал прийнятий (received signal)

Інформацію в байті стану можна представити за допомогою наступного бітового


поля:

struct status_type {
unsigned delta_cts: 1;
unsigned delta_dsr: 1;
unsigned tr_edge: 1;
unsigned delta_rec: 1;
unsigned cts: 1;
unsigned dsr: 1;
unsigned ring: 1;
unsigned rec_line: 1;
} status;

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

status = get_port_status();
if (status.cts) printf("Дозвіл на передачу");
if (status.dsr) printf("Дані готові");

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


для елемента, що знаходиться в структурі будь-якого іншого типу. Ось, наприклад,
фрагмент коду, що виконує скидання поля ring:

status.ring = 0;

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


крапка. Однак якщо структура передана за допомогою покажчика, то слід
використовувати операцію стрілка ->.
Немає необхідності давати ім'я кожному бітовому полю. Таким чином можна
легко отримувати доступ до потрібного біту, обходячи невикористовувані.
- 135 -
Наприклад, якщо нас цікавлять тільки біти cts і dsr, то структуру status_type можна
оголосити таким чином:

struct status_type {
unsigned: 4;
unsigned cts: 1;
unsigned dsr: 1;
} status;

Крім того, якщо біти, розташовані після dsr, не використовуються, то визначати їх


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

struct emp {
struct addr address;
float pay;
unsigned lay_off: 1; / * Тимчасово звільнений або працює */
unsigned hourly: 1; /* погодинна оплата або оклад */
unsigned deductions: 3; /* процент податкових утримань * /
};

визначені дані про працівника, для яких виділяється тільки один байт, що містить
інформацію трьох видів: статус працівника, чи на окладі він, а також процент
утримань із його зарплати. Без бітового поля ця інформація займала б 3 байта.

Приклад 8.05. Доступ до окремих бітів даних за допомогою бітових полів

#define _CRT_SECURE_NO_WARNINGS
#include "stdlib.h"
#include "stdio.h"
#include "conio.h"
#include "math.h"
#include "locale.h"
#include "string.h"
#include "windows.h"

struct tbyte {
unsigned a0 : 1;
unsigned a1 : 1;
unsigned a2 : 1;
unsigned a3 : 1;
unsigned a4 : 1;
unsigned a5 : 1;
unsigned a6 : 1;
unsigned a7 : 1;
};

void main() {
struct tbyte x = { 0, 0, 0, 1, 0, 0, 0, 0 };
x.a1 = 1;
printf("size of tbyte = %d\n", sizeof(struct tbyte));
- 136 -
printf("x.a1 = %d\n", x.a1);
printf("x.a3 = %d\n", x.a3);
printf("x.a5 = %d\n", x.a5);
}

У цьому прикладі кожне поле структури визначено як бітове поле, довжина кожного
поля дорівнює одному бітові. Звертатися до кожного поля можна також, як і до поля
звичайної структури. Бітові поля мають тип unsigned int, так як вказується довжина
поля в один біт. Якщо довжина поля більше одного біта, то поле може мати і
знаковий цілий тип.

Приклад 8.06. Бітове поле зі знаковими типами

#define _CRT_SECURE_NO_WARNINGS
#include "stdlib.h"
#include "stdio.h"
#include "conio.h"
#include "math.h"
#include "locale.h"
#include "string.h"
#include "windows.h"

struct tbyte {
signed char a : 4;
signed char b : 4;
};

void main() {
struct tbyte x = { -1, 3 };
printf("hex a = %x\n", x.a);
printf("dec a = %d\n", x.a);
printf("hex b = %x\n", x.b);
printf("dec b = %d\n", x.b);
}
Ця програма буде друкувати наступне:

hex a = ffffffff
dec a = -1
hex b = 3
dec b = 3

Як можна бачити, шістьнадцяткове від'ємне значення змінної а "трохи"


відрізняється від звичного виду. Спробуйте пояснити це.
Працювати з бітовими полями, ініціалізуючи кожне поле окремо, незручно. Тому
структури з бітовими полями часто роблять полем об'єднання, наприклад:

Приклад 8.07. Спільне використання бітових полів та об'єднання

#include <conio.h>
#include <stdio.h>

struct tByte {
- 137 -
unsigned a0 : 1;
unsigned a1 : 1;
unsigned a2 : 1;
unsigned a3 : 1;
unsigned a4 : 1;
unsigned a5 : 1;
unsigned a6 : 1;
unsigned a7 : 1;
};

union uByte {
unsigned char value;
struct tByte bitfield;
};

void main() {
union uByte x;
x.value = 10;
printf("%d%d%d%d%d%d%d%d\n", x.bitfield.a7, x.bitfield.a6, x.bitfield.a5,
x.bitfield.a4, x.bitfield.a3,
x.bitfield.a2, x.bitfield.a1, x.bitfield.a0);
}

Тоді ми одним оператором x.value = 10 можемо присвоїти значення усім бітовим


полям структури:

00001010

Використання бітових полів має певні обмеження:


- не можна отримати адресу бітового поля;
- не можна створювати масив бітових полів;
- при перенесенні коду на іншу платформу невідомо, чи будуть поля оброблятися
справа наліво або зліва направо. Це означає, що виконання будь-якого коду, в
якому використовуються бітові поля, певною мірою може залежати від
платформи, на якій він виконується;
- інші обмеження можуть залежати від конкретних реалізацій.
8.8. Нумератор (Лічений тип)
Лічений тип, або нумератор - це перелік іменованих цілих констант. Перелік
досить часто зустрічається в повсякденному житті. Наприклад, імена днів тижня:

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

Нумератор оголошується багато в чому так само, як і структури: початком


оголошення нумератора слугує ключове слово enum. Оголошення нумератора в
загальному вигляді виглядає так:

enum тег {список переліку} список змінних;

Тут тег і список змінних не є обов'язковими (але хоча б щось одне з них повинно
бути присутнім!). Наступний фрагмент коду визначає нумератор з ім'ям day_weekly
(день тижня):
- 138 -

enum day_weekly {Monday, Tuesday, Wednesday, Thursday, Friday, Saturday,


Sunday};

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


ліченого типу. Ось код, в якому day (день) оголошується в якості змінної типу
day_weekly:

enum day_weekly day;

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

day = Tuesday;
if (day == Sunday) printf ( "Сьогодні неділя. \n");

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


являє собою ціле число. У такому вигляді елементи списка переліка можна
застосовувати всюди, де використовуються цілі числа. Кожному наступному
елементу списку переліка дається значення, на одиницю більше, ніж у його
попередника. Перший елемент списка має значення 0. Тому, при виконанні коду

printf("%d %d", Monday, Wednesday);

на екран буде виведено 0 2.


Однак для одного або більше елементів списку переліку можна вказати
значення, яке слід використовувати як ініціалізатор, замість значення за
замовченням. Для цього після імені елемента в списку переліку слід поставити знак
рівності, а потім – потрібне ціле значення. Елементам переліку, які йдуть після
ініціалізатора, будуть автоматично присвоюватися значення, більші від
попереднього на 1. Наприклад, наступний код присвоює Thursday значення 100:

enum day_weekly {Monday, Tuesday, Wednesday, Thursday=100, Friday,


Saturday, Sunday};

Тепер значення елементів списку переліку будуть наступні:


Monday 0
Tuesday 1
Wednesday 2
Thursday 100
Friday …... 101
Saturday 102
Sunday 103

Щодо змінних нумерованого типу є одна поширена, але помилкова думка. Вона
полягає в тому, що їх елементи можна безпосередньо вводити і виводити. Це не
так. Наприклад, наступний фрагмент коду не виконуватиметься так, як того очікують
багато недосвідчених програмістів:

/ *Цей код працювати не буде * /


day = Monday;
printf("%s", day);
- 139 -
Тут Monday - це ім'я для значення цілого типу; а не ім'я рядка. Таким чином,
спроба вивести day у вигляді рядка по суті успіху не матиме. З тієї ж причини для
досягнення потрібних результатів не годиться і такий код:

/ *Цей код не правильний * /


strcpy(day, "Monday");

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


перетвориться.
Насправді ж створювати код для введення і виведення елементів переліку - це
досить-таки трудомістке заняття. Наприклад, щоб виводити назву дня тижня,
використовуючи змінну day, буде потрібен наступний код:

switch (day) {
case Monday: printf("Понеділок");
break;
case Tuesday: printf("Вівторок");
break;
і т.д.;
}

Іноді можна оголосити масив рядків і використовувати значення змінної-


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

char name[][11] = {
"Понеділок",
"Вівторок",
"Середа",
"Четвер",
"П'ятниця",
"Субота",
"Неділя"
};
day = Wednesday;
printf("%s", name[day]);

Так як при операціях введення / виведення необхідно спеціально піклуватися про


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

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

#define _CRT_SECURE_NO_WARNINGS
#include "stdlib.h"
#include "stdio.h"
#include "conio.h"
#include "math.h"
#include "locale.h"
#include "string.h"
- 140 -
#include "windows.h"

static char *ErrorNames[] = {


"Індекс поза меж діапазону\n",
"Переповнення стека\n",
"Спроба доступу до даних пустого стека\n",
"Недостатньо пам'яті\n"
};

enum Errors {
INDEX_OUT_OF_BOUNDS = 1,
STACK_OVERFLOW,
STACK_UNDERFLOW,
OUT_OF_MEMORY
};

void main() {
SetConsoleCP(1251);
SetConsoleOutputCP(1251);
//сталася помилка
printf(ErrorNames[INDEX_OUT_OF_BOUNDS-1]);
exit(INDEX_OUT_OF_BOUNDS);
}

8.9. Використання операції sizeof для забеспечення кросплатформеності


Структури і об'єднання можна використовувати для створення змінних різних
розмірів, але слід зазначити, що поточний розмір цих змінних на різних машинах та
в операційних системах може бути різним. Це пояснюється тим, що всередині самої
структурної змінної на різних системах можуть діяти різні правила вирівнювання
даних. Для того, щоб програміст завжи мав інформацію про поточний розмір
структури слід використовувати операцію sizeof, яку було описано в п. 3.3. Операція
sizeof підраховує розмір будь-якої змінної або будь-якого типу і може бути
корисною, якщо в програмах потрібно звести до мінімуму машинно-залежний код.
Ця операція особливо корисна там, де доводиться мати справу зі структурами або
об'єднаннями.
Припустимо, що певні типи даних мають такі розміри:

Тип Розмір в байтах


char 1
int 4
double 8

При виконанні наступного коду на екран будуть виведені числа 1, 4 і 8:

char ch;
int i;
double f;

printf("%d", sizeof(ch));
printf("%d", sizeof(i));
printf("%d", sizeof(f));
- 141 -

Як було вказано раніше, розмір структури дорівнює сумі розмірів її членів або,
можливо, може бути навіть більшим від цієї суми. Розглянемо приклад:

struct s {
char ch;
int i;
double f;
} s_var;

Тут sizeof(s_var) дорівнює як мінімум 13 (= 8 + 4 + 1). Однак розмір s_var може


бути і більшим, тому що компілятор іноді спеціально збільшує розмір структури для
того, щоб вирівняти деякі її члени на границю слова (чотири байти) або параграфа
(16 байтів). Так як розмір структури може бути більшим, ніж сума розмірів її членів,
то завжди, коли програмі потрібно знати розмір структури, слід використовувати
оператор sizeof. Наприклад, якщо потрібно динамічно виділяти пам'ять для об'єкта
типу s, необхідно використовувати послідовність операторів, аналогічну тій, що
показана тут (а не вставляти вручну значення довжини його членів):

struct s *p;
p = malloc(sizeof(struct s));

Так як sizeof - це операція часу компіляції, то вся інформація, необхідна для


обчислення розміру будь-якої змінної, стає відомою як раз під час компіляції. Це
особливо важливо для об'єднань, тому що розмір кожного з них завжди дорівнює
розміру найбільшого члена. Наприклад, проаналізуємо наступне об'єднання:

union u {
char ch;
int i;
double f;
} u_var;

Для нього sizeof(u_var) дорівнює 8. Втім, під час виконання не має значення, який
розмір насправді має u_var. Важливий розмір його найбільшого члена, так як будь-
яке об'єднання має бути такого ж розміру, як і його найбільший елемент.
8.10. Визначення нових типів з використанням ключового слова typedef
Нові імена типів даних в мові Сі можна визначати, використовуючи ключове
слово typedef. Насправді таким способом новий тип даних не створюється, а всього
лише визначається нове ім'я для вже існуючого типу.
Загальний вигляд декларації typedef (оператора typedef) такий:

typedef тип нове_ ім'я;

де тип - це будь-який тип даних мови Сі, а нове_ім’я - нове ім'я цього типу. Нове
ім'я є доповненням до вже існуючого, а не його заміною.
Наприклад, для типу unsigned int можна створити нове ім'я за допомогою

typedef unsigned int uint


- 142 -
Цей оператор дає компілятору вказівку вважати uint синонімом
словосполучення unsigned int. Надалі, використовуючи uint, можна створити
змінну типу unsigned int:

uint m;

Тепер є беззнакова цілочисельна змінна m типу uint, a uint є ще одним ім'ям типу
unsigned int.
Ще приклад, але вже зі складними визначеннями структур:

struct addr {
char name[30];
char street[40];
char city[20];
unsigned int zip;
} addr_list[MAX];

Тут, кожен раз при визначенні нової змінної адреси потрібно виписувати і ім'я тега
структури, або саму структуру, якщо вона без тега. А коли таких оголошень кілька в
різних частинах програми? Скажімо, в прототипах функцій, в оголошенні функцій і в
самому тілі програми? Для виходу з ситуації використовуємо typedef:

typedef unsigned int uint;


typedef struct {
char name[30];
char street[40];
char city[20];
uint zip;
} tAddr;

Тепер, усюди, де нам необхідно оголосити структурну змінну адреси досить буде
просто написати:

tAddr addr_list;

або (якщо масив):

tAddr addr_list[MAX];

З прикладів видно, що typedef ніби замінює останнє слово декларації на всі


слова що знаходяться між typedef і останнім словом. Крім того, як видно з
останнього прикладу, можливе використання "вкладеного" typedef при описі поля
zip структури.
Це може виявитися корисним при написанні програм з таких міркувань:
- по-перше, можна зробити машинно-залежні програми більш мобільними.
Якщо для кожного машинно-залежного типу даних, використовуваного в
програмі, доводиться визначати нове ім'я, то при компіляції для нового
середовища доведеться міняти тільки "начинку" оператора typedef;
- по-друге, такі вирази можуть допомогти в самодокументуванні коду,
дозволяючи давати зрозумілі імена складним типам даних;
- 143 -
- по-третє, використання typedef дозволяє перетворити довге оголошення
будь-якого типу в одному місці (в межах видимості!) в коротке і/або змістовне
оголошення в багатьох місцях програми.
- 144 -

ТЕМА 9. ВВЕДЕННЯ / ВИВЕДЕННЯ У МОВІ СІ

У мові Сі (на відміну від інших мов програмування) не визначено ніяких


додаткових операторів, за допомогою яких можна виконувати операції введення /
виведення в програмі. Замість них використовуються стандартні бібліотечні функції,
що забезпечує дуже гнучкий механізм передачі даних від одного пристрою до
іншого. Система ця досить велика і складається з багатьох різних функцій.
Заголовковим файлом для всіх функцій введення / виведення є файл stdio.h.
Є як консольні, так і файлові функції введення / виведення. Консольними
функціями введення / виведення називаються ті, які виконують введення з
пристрою, подібного клавіатурі і виведення на пристрій, подібний екрану. Тобто,
здійснюють введення / виведення в формі, зручній для сприйняття людиною.
Файлові функції можуть здійснювати введення / виведення без перетворення, тобто
в двійковому форматі, або, як і консольні - в текстовому форматі, з перетворенням.
Слід зазначити, що в стандарті мови Сі не визначені жодні функції, призначені
для виконання різних операцій управління екраном (наприклад, позиціонування
курсора) або виведення на нього графічної інформації. І не визначені тому, що ці
операції на різних машинах дуже сильно відрізняються. Крім того, в стандартному
Сі не визначені жодні функції, які виконують операції виведення в звичайному або
діалоговому вікні, створюваному в середовищі Windows. Функції введення /
виведення на консоль виконують всього лише телетайпне введення / виведення.
Однак в бібліотеках більшості компіляторів є відповідні функції графікі та управління
екраном, призначені для того середовища, в якому якраз і повинні виконуватися
програми.
Консольні функції працюють зі стандартним потоком введення і стандартним
потоком виведення. Стандартне введення - це логічний файл для введення даних,
що зв'язується при запуску програми з фізичним файлом або стандартним
виведенням іншої програми. За замовчуванням стандартне введення в пакетному
режимі зв'язується з вхідним потоком, а в діалоговому режимі - з терміналом.
Стандартне виведення - логічний файл виведення даних, що зв'язується при
запуску з фізичним файлом або стандартним введенням іншої програми. За
замовчуванням стандартне виведення в пакетному режимі зв'язується з вихідним
потоком, а в діалоговому режимі - з терміналом. Стандартне введення і стандартне
виведення можуть бути перенаправлені на інші пристрої. Таким чином, "консольні
функції" не обов'язково повинні працювати тільки з консоллю. Перенаправлення
вводу / виводу буде вивчено в наступних лекціях.
Так як Сі є фундаментом C++, то іноді виникає плутанина у відносинах його
файлової системи з аналогічною системою C++. По-перше, C++ підтримує всю
файлову систему Сі. Таким чином, при переміщенні старого Сі-коду в C++ немає
необхідності міняти всі процедури введення / виведення. По-друге, слід мати на
увазі, що в C++ визначена своя, об'єктно-орієнтована система введення /
виведення, в яку входять як функції, так і оператори введення / виведення. В
системі вводу / виводу C++ повністю підтримуються всі можливості аналогічної
системи Сі і це робить зайвим використання файлової системи мови Сі при
написанні об'єктно-орієнтованих програм. Взагалі кажучи, при написанні програм на
мові C++, зазвичай більш зручно використовувати саме його систему введення /
виведення, але, якщо необхідно скористатися файловою системою мови Сі, то це
також цілком можливо.
9.1. Зчитування і запис окремих символів
Найпростішими з консольних функцій введення / виведення є getchar(), яка
читає символ з клавіатури, і putchar(), яка відображає символ на екрані. Перша з цих
- 145 -
функцій очікує, поки ви не натиснете <ENTER>, а потім повертає значення першої
клавіші в буфері, не очищуючі самого буфера. Крім того, при натисканні чергової
клавіші на клавіатурі на екрані автоматично відображається відповідний символ.
Друга ж функція, putchar(), відображає символ на екрані в поточній позиції курсору.
Ось прототипи функцій getchar() і putchar():

int getchar(void);
int putchar(int c);

Як видно з прототипу, вважається, що функція getchar() повертає цілий


результат. Повернуте значення можна присвоїти змінній типу char, як зазвичай і
робиться, так як символ міститься в молодшому байті. (Старший байт при цьому
зазвичай обнулений). У випадку помилки getchar() повертає EOF. (Макрос EOF
визначається в <stdio.h> і часто дорівнює -1).
Що ж стосується putchar(), то незважаючи на те, що ця функція оголошена як та,
що приймає цілий параметр, вона зазвичай викликається з символьним
аргументом. Насправді з її аргументу на екран виводиться тільки молодший байт.
Функція putchar() повертає записаний символ або, в разі помилки, EOF.

Труднощі використання getchar() і альтернативи

Використання getchar() може бути пов'язано з певними труднощами. У багатьох


бібліотеках компіляторів ця функція реалізується таким чином, що вона заповнює
буфер введення до тих пір, поки Ви не натиснете <ENTER>. Це називається
порядково буферизованим вводом. Щоб функція getchar() повернула який-небудь
символ, необхідно натиснути клавішу <ENTER>. Крім того, ця функція при кожному
її виклику вводить тільки по одному символу. Тому збереження в буфері рядка
символів, що залишився, може привести до того, що в черзі на введення
залишаться чекати один або кілька символів, що може привести до невірного
наступного введення і вимагатиме додаткової операції попередньої чистки буфера.
Тому для читання символів з клавіатури може знадобитися інша функція. У
стандарті мови Сі не визначається ніяких функцій, які гарантували б інтерактивне
введення, але їх визначення є буквально у всіх бібліотеках використовуваних
компіляторів Сі.
У двох з найпоширеніших альтернативних функцій getch() і getche() є наступні
прототипи:

int getch(void);
int getche(void);

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


заголовочному файлі <conio.h>. У бібліотеках деяких компіляторів, наприклад Visual
C++ компанії Microsoft, імена цих функцій починаються зі знака підкреслення:
_getch() і _getche().
Функція getch() очікує натискання клавіші, після якого вона негайно повертає
значення. Причому, символ, введений з клавіатури, на екрані не відображається.
Функція getche() така ж, як getch(), але символ на екрані відображається (кажуть, що
функція виконує введення символу з ехом). Приклад програми, що виконує
введення символів з екрану і змінює регістр символів (може не обробляти
кирилицю!), з використанням функції getch():
- 146 -
Приклад 9.01 Введення символів з консолі і зміна регістру символів з
подальшим їх виведенням на консоль

#include <stdio.h>
#include <conio.h>
#include <ctype.h>
int main(void) {
char ch;
printf("Введіть якийсь текст. (Для завершення роботи введіть крапку). \n ");
do {
ch = getch();
if(islower(ch))
ch = toupper(ch);
else
ch = tolower(ch);
putchar(ch);
} while (ch != '.');
return 0;
}

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


символ відразу передається програмі і виводиться на іншому регістрі. А введення
в рядку не буферизується.
9.2. Зчитування та запис рядків символів
Серед функцій введення / виведення на консоль є і більш потужні: це функції
gets() і puts(), які дозволяють зчитувати і відображати рядки символів.
Функція gets() читає рядок символів, введений з клавіатури, і записує його в
пам'ять за адресою, на яку вказує її аргумент. Символи можна вводити з клавіатури
до тих пір, поки не буде введений символ повернення каретки '\n'. Він не стане
частиною рядка, а замість нього в кінець введеного рядка буде добавлено символ
кінця рядка '\0', після чого відбудеться повернення з функції gets(). Слід мати на
увазі, що повернути символ '\n' за допомогою цієї функції не можна. Перед тим як
натискати <ENTER>, можна виправляти неправильно введені символи,
користуючись для цього клавішею повернення каретки на одну позицію (клавішею
backspace). Прототип для gets() виглядає так:

char* gets (char* cmp);

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


вводиться користувачем, додатково ще gets() також повертає цей же покажчик в
своєму імені. Наступна програма читає рядок у масив str і виводить його довжину:

Приклад 9.02. Програма читає рядок у масив str і виводить його довжину

#include <stdio.h>
#include <string.h>

int main(void) {
char str[80];
gets(str);
printf("Довжина в символах дорівнює %d", strlen(str));
- 147 -
return 0;
}

Зараз функція gets() використовується лише в учбових демонстраційних


програмах, тому що ця функція не перевіряє границю масиву, в який записуються
введені символи рядка. Таким чином, може статися так, що користувач введе
більше символів, ніж розмір масиву, що призведе до неправильної роботи програми
чи її аварійної зупинки. Тому в професійних програмах нею не користуються. Її
альтернативою, що дозволяє запобігти переповненню масиву, буде функція fgets(),
яку ми вивчимо далі.

Функція puts() відображає на екрані свій строковий аргумент, після чого курсор
переходить на новий рядок. Прототип цієї функції має такий вигляд:

int puts (const char* cmp);

Функція puts() використовує ті ж самі керуючі послідовності, що і printf(), наприклад,


'\t' в якості символу табуляції. Виклик функції puts() вимагає набагато менше
ресурсів, ніж виклик printf(). Це пояснюється тим, що puts() може тільки виводити
рядок символів, але не може виводити числа або робити перетворення формату. В
результаті ця функція займає менше місця в пам'яті і виконується швидше, ніж
printf(). Тому тоді, коли не потрібні перетворення формату, часто використовується
саме puts(). Функція puts() в разі успішного завершення повертає позитивне
значення, а в разі помилки - EOF.

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


виведення, що ми вже знаємо.

Таблиця 9.1. Перелік основних функцій консольного вводу/виводу

Функція Її дії

getchar() Зчитує символ з клавіатури; зазвичай очікує натискання клавіші <ENTER>

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


getche()
клавіші <ENTER>; в стандарті Сі не визначена, але поширена досить широко

Зчитує символ, але не відображає його на екрані; не очікує натискання


getch()
клавіші <ENTER>; в стандарті Сі не визначена, але поширена досить широко

putchar() Виводить один символ на екран

gets() Зчитує рядок з клавіатури

puts() Виводить рядок на екран

У наступній програмі (простому комп'ютеризованому словнику) показано


застосування декількох основних функцій консольного введення / виведення. Ця
- 148 -
програма пропонує користувачеві ввести слово, а потім перевіряє, чи збігається
воно з будь-яким з тих слів, що знаходяться в її базі даних. Якщо воно там є, то
програма виводить значення цього слова. Зверніть особливу увагу на використання
непрямої адресації в цій програмі. Щоб легше було зрозуміти програму, нагадаємо,
що масив dic - це масив покажчиків на рядки. Зверніть увагу, що список має
завершуватися двома нулями.

Приклад 9.03. Програма простого словника

#define _CRT_SECURE_NO_WARNINGS
#include "stdlib.h"
#include "stdio.h"
#include "conio.h"
#include "math.h"
#include "locale.h"
#include "string.h"
#include "windows.h"

/* Список слів та їх значень */


char *dic[][40] = {
"Атлас", "Збірник географічних або топографічних карт.",
"Автомобіль", "Моторизований засіб пересування.",
"Телефон", "Засіб зв'язку.",
"Літак", "Літаюча машина з двигуном.",
"", "" // Нулі,що завершують список
};

int main(void) {
char word[80], ch;
char **p;

SetConsoleCP(1251);
SetConsoleOutputCP(1251);

do {
puts("\nВведіть слово: ");
scanf("%s", word);

p = (char **)dic;

/*Пошук слова в словнику і виведення його значення */


do {
if (!strcmp(*p, word)) {
puts("Значення:");
puts(*(p + 1));
break;
}
p = p + 2; /* просування по списку */
} while (*p);

if (!*p) puts("Слово в словнику відсутнє.");


puts("Будете ще вводити? (y/n): ");
- 149 -
ch=_getch();
} while (toupper(ch) != 'N');

return 0;
}

9.3. Функція форматного виведення на консоль printf()


Для форматного виведення символьної інформації з програми на консоль
послуговує функція printf(). Ось прототип функції printf():

int printf (const char* керуючий_рядок, аргумент1, ..., аргументN);

Функція printf() після свого відпрацювання повертає у своєму імені число виведених
символів або негативне значення в разі помилки.
Перший параметр функції - керуючий_рядок, складається з елементів двох
видів. Перший з них - це символи, які належить вивести на екран; другий - це
специфікатори форматного перетворення, які визначають спосіб виведення
аргументів, що відповідають цим специфікаторам. Кожен такий специфікатор
починається зі знака процента, за яким слідує код формату.
Аргументів має бути рівно стільки, скільки і специфікаторів перетворення,
причому специфікатор і аргументи повинні попарно відповідати один одному в
напрямку зліва направо. Наприклад, в результаті такого виклику printf()

printf ( "Мені подобається мова%c %s", ‘C’, "і до того ж, дуже сильно!");

буде виведено

Мені подобається мова C і до того ж дуже сильно!

У цьому прикладі першому специфікатору перетворення %c, відповідає символ 'C',


а другому %s, - рядок "і до того ж дуже сильно!".
У функції printf(), як видно з таблиці нижче, є широкий набір специфікаторів
перетворення.
Таблиця 9.2. Перелік специфікаторів перетворення функції printf()

Код Формат
%c Символ
%d Десяткове представлення цілого зі знаком
Десяткове представлення цілого зі знаком. Для функції printf() дія обох
%i специфікаторів %d і %i повністю однакова. Відмінності існують лише для функції
scanf()
%e Експоненціальне представлення числа ('е' на нижньому регістрі)
%E Експоненціальне представлення числа ('Е' на верхньому регістрі)
%f Десяткове з плаваючою точкою
%g Залежно від того, який вивід буде коротшим, використовується %е або %f
%G Залежно від того, який вивід буде коротшим, використовується %Е або %f
%o Вісімкове представлення цілого без знака
%s Рядок символів
- 150 -

Код Формат
%u Десяткове представлення цілого без знака
%x Шістнадцяткове представлення цілого без знака (літери на нижньому регістрі)
%X Шістнадцяткове представлення цілого без знака (літери на верхньому регістрі)
Виводить представлення покажчика (форма представлення залежить від
%p
платформи!)
Аргумент, що відповідає цьому специфікатору, повинен бути покажчиком
на цілочисельну змінну. Специфікатор дозволяє зберегти в цій змінній
%n кількість записаних (вже виведених) символів поточною функцією printf()
(записаних до того місця, з якого починається запис в змінну, що відповідає
специфікатору %n).
%% Виводить знак %

Виведення символів

Для виведення окремого символу використовується специфікатор %с. В


результаті відповідний аргумент (один символ) буде виведений на екран без зміни.
Для виведення рядка використовується специфікатор %s.

Виведення чисел

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


специфікатора перетворення %d або %i. Ці специфікатори перетворення
еквівалентні; обидва підтримуються в силу сформованих традицій, наприклад,
через бажання підтримувати ті ж специфікатори, які застосовуються в функції
scanf().
Для виведення цілого значення без знака використовується %u. Попередження:
намагання вивести беззнакове ціле за допомогою специфікаторів %d або %i
призведе до невірного виведення, чи навіть, до помилки в програмі!
Специфікатор перетворення %f дає можливість виводити дійсні числа (числа з
дробовою частиною) в форматі з фіксованою позицією точки. Відповідний аргумент
повинен мати "плаваючий" тип.
Специфікатори перетворення %e і %E в функції printf() дозволяють відображати
"плаваючий" аргумент в експоненціальному форматі. У загальному вигляді числа в
такому форматі виглядають наступним чином:

x.ddddd[e/E][+/-]yy

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


перетворення %E; в іншому випадку - використовувати специфікатор перетворення
%e.
Специфікатор перетворення %g або %G вказує, що функції printf() необхідно
самостійно вибрати один із специфікаторів: %f, або %e, або %Е. В результаті printf()
вибере той специфікатор перетворення, який дозволяє зробити найкоротше
представлення числа при виведенні. Якщо потрібно, щоб при автоматичному виборі
експоненціального формату буква E відображалася на верхньому регістрі, слід
використовувати специфікатор перетворення %G; в іншому випадку -
використовуйте специфікатор перетворення %g. Приклад застосування
специфікатора перетворення %g показано в наступній програмі:
- 151 -

Приклад 9.04. Застосування специфікатора перетворення %g

#include <stdio.h>
int main(void) {
double f;
for(f=1.0; f<1.0e+10; f=f*10)
printf("%g ", f);
return 0;
}

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

1 10 100 1000 10000 100000 1e+06 1e+07 1e+08 1e+09

Цілі числа без знака можна виводити у вісімковому або шістнадцятковому


форматі, використовуючи специфікатор перетворення %o або %x (%X). Так як в
шістнадцятковій системі для представлення чисел від 10 до 15 використовуються
літери від А до F, то ці букви можна виводити на верхньому або на нижньому
регістрі. У першому випадку використовується специфікатор перетворення %X, а в
другому - специфікатор перетворення %x:

Приклад 9.05. Робота зі специфікаторами перетворення o/x/X

#include <stdio.h>
int main(void) {
unsigned num;
for (num=0; num < 16; num++) {
printf("%o ", num);
printf("%x ", num);
printf("%X\n", num);
}
return 0;
}

Ось що виведе ця програма:

000
111
222
333
444
555
666
777
10 8 8
11 9 9
12 a A
13 b B
14 c C
15 d D
16 e E
- 152 -
17 f F

Відображення значення покажчика (адреси)

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


перетворення %p. Цей специфікатор перетворення дає printf() вказівку відобразити
машинну адресу в форматі, сумісному з адресацією, яка використовується
комп'ютером, у шістьнадцятирічному вигляді. Наступна програма відображає
адресу змінної sample:

#include <stdio.h>
int sample;
int main(void {
printf("%p", &sample);
return 0;
}

Для 32-разрядних систем адреса може бути виведеною у вигляді одного


восьмиразрядного числа виду 0AA123FD, для 16-разрядних систем – у вигляді пари
чотирирозрядних чисел 0234:A100.

Специфікатор перетворення %n

Специфікатор %n значно відрізняється від інших специфікаторів перетворення.


Коли функція printf() зустрічає його, нічого не виводиться. Замість цього виконується
зовсім інша дія: в цілу змінну, зазначену відповідним цьому специфікаторові
аргументом функції, записується кількість виведених до цього моменту поточною
функцією printf() символів. Іншими словами, значення, яке відповідає специфікатору
перетворення %n, має бути покажчиком на цілу змінну (бо в неї виконується запис).
Після завершення виклику printf() в цій змінній буде зберігатися кількість символів,
виведених до того моменту, коли зустрівся специфікатор перетворення %n. Щоб
усвідомити сенс цього трохи незвичайного специфікатора перетворення,
розберемся, як працює наступна програма:

Приклад 9.06 Виведення зі специфікатором %n

#include <stdio.h>
#include <windows.h>

int main(void) {
int count;

SetConsoleCP(1251);
SetConsoleOutputCP(1251);

_set_printf_count_output(1);
printf("Це %n перевірка\n", &count);
_set_printf_count_output(0);
printf("%d\n", count);
return 0;
}
- 153 -
Програма відображає рядок "Це перевірка", після якого з'являється рядок з числом
3. Специфікатор перетворення %n в основному використовується в програмі для
виконання динамічного форматування при виведенні складних таблиць чи даних.

Модифікатори формату

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


дещо змінюють їх значення. Наприклад, можна вказувати мінімальну ширину поля,
кількість десяткових розрядів і вирівнювання по лівому краю. Модифікатор формату
завжди ставлять між знаком процента і кодом формату.

Модифікатори мінімальної ширини поля

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


модифікатора мінімальної ширини поля. Якщо вказано модифікатор мінімальної
ширини поля, то ширина поля виведення буде не меншою зазначеної мінімальної
довжини, при необхідності вивід буде доповнений пробілами. Якщо ж виводяться
рядки або числа, які довше зазначеного мінімуму, то вони все одно будуть
відображатися повністю. За замовчуванням для доповнення використовуються
пробіли. А якщо для цього треба використовувати нулі, то перед модифікатором
ширини поля слід помістити 0. Наприклад, %05d означає, що будь-яке число,
кількість цифр якого менше п'яти, буде доповнено такою кількістю нулів, щоб число
складалося з п'яти цифр. У наступній програмі показано, як застосовується
модифікатор мінімальної ширини поля:

#include <stdio.h>
int main(void) {
double item;
item = 10.12304;

printf("%f\n", item);
printf("%10f\n", item);
printf("%012f\n", item);

return 0;
}

Ось що виводиться при виконанні цієї програми:

10.123040
10.123040
00010.123040

Модифікатор мінімальної ширини поля найчастіше використовується при створенні


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

Модифікатори точності

Модифікатор точності (значущих цифр) слідує за модифікатором мінімальної


ширини поля (якщо такий є). Він складається з точки і розташованого за нею цілого
числа. Значення цього модифікатора залежить від типу даних, до яких його
застосовують.
- 154 -
Коли модифікатор точності застосовується до даних з плаваючою точкою, для
перетворення яких використовуються специфікатор перетворення %f, %e або %E,
то він визначає кількість виведених десяткових розрядів після коми. Наприклад,
%10.4f означає, що ширина поля виведення буде не менше за 10 символів, причому
для десяткових розрядів після коми буде відведено лише чотири позиції.
Якщо модифікатор точності застосовується до %g або %G, то він визначає
кількість значущих цифр у виведеному числі.
Застосований до рядків, модифікатор точності визначає максимальну довжину
поля. Наприклад, %5.7s означає, що довжина рядка, що виводиться становитиме
мінімум п'ять і максимум сім символів. Якщо рядок виявиться довшим, ніж
максимальна довжина поля, то кінцеві символи виводитися не будуть.
Якщо модифікатор точності застосовується до цілих типів, то він визначає
мінімальну кількість цифр, які будуть виведені для кожного з чисел. Щоб вийшла
необхідна кількість цифр, додається певна кількість ведучих нулів.
У наступній програмі показано, як можна використовувати модифікатор
точності:

#include <stdio.h>
int main(void) {
printf("%.4f\n", 123.1234567);
printf("%.4E\n", 123.1234567);

printf("%.4G\n", 123.1234567);
printf("%3.8d\n", 1000);
printf("%10.15s\n", "Це проста перевірка.");
return 0;
}

Ось що виводиться при виконанні цієї програми:

123.1235
1.2312E+02
123.1
00001000
Це проста перев

Вирівнювання значення в полі виведення

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


Тобто якщо ширина поля більше ширини виведених даних, то ці дані
розташовуються по правому краю поля. Вивід по лівому краю можна призначити
примусово, помістивши знак мінус прямо за знаком специфікатора %. Наприклад,
%-10.2f означає, що число з плаваючою точкою і з двома десятковими розрядами
після коми буде вирівняно по лівому краю 10-символьного поля.
У наступній програмі показано, як застосовується вирівнювання по лівому краю:

#include <stdio.h>
int main(void) {
printf(".........................\n");
printf("по правому краю: %8d\n", 100);
printf(" по лівому краю: %-8d\n", 100);
return 0;
- 155 -
}

І ось що вийшло:
.........................
по правому краю: 100
по лівому краю: 100

Обробка даних інших типів

Деякі модифікатори у виклику функції printf() дозволяють відображати цілі числа


типу short і long. Такі модифікатори можна використовувати для наступних
специфікаторів типу: d, i, o, u, x. Модифікатор l (ель) у виклику функції printf() вказує,
що за ним слідують дані типу long. Наприклад , %ld означає, що треба виводити
дані типу long int. Після модифікатора h функція printf() виведе ціле значення у
вигляді short. Наприклад, %hu означає, що виведені дані мають тип short unsigned
int.
Модифікатори l і h можна також застосувати до специфікаторів %n. Це робиться
з тією метою, щоб показати - відповідний аргумент є покажчиком відповідно на довге
long або коротке short ціле.
Модифікатор L може стояти перед специфікаторами перетворення з плаваючою
точкою e, f, g, і вказувати цим, що виводиться значення long double.

Модифікатор * и #

Для деяких зі своїх специфікаторів перетворення функція printf() підтримує два


додаткових модифікатора: * і #.
Безпосереднє розташування # перед специфікаторами перетворення g, G, f, Е,
e означає, що при виведенні обов'язково з'явиться десяткова точка - навіть якщо
десяткових цифр після коми немає. Якщо ви поставите # безпосередньо перед x, X,
то шістнадцяткове число буде виведено з префіксом 0x. Якщо # буде
безпосередньо передувати специфікатору перетворення o (%#o), то число буде
виведено з ведучим нулем. До будь-яких інших специфікаторів перетворення
модифікатор # застосовувати не можна.
Модифікатори мінімальної ширини поля і точності можна передавати функції
printf() не як константи, а як аргументи. Для цього як заповнювач, на місці
відповідного модифікатора слід використовувати зірочку * замість константи. При
скануванні рядка формату функція printf() буде кожній зірочці * з цього рядка ставити
у відповідність черговий аргумент зі списку аргументів функції printf(), причому в
тому порядку, в якому розташовані аргументи. Наприклад, при виконанні оператора
printf() з наступного прикладу, мінімальна ширина поля буде дорівнювати 10
символам, точність - 4, а відображатися буде число 123.3456.
У наступній програмі показано застосування обох модифікаторів # і *:

Приклад 9.10. Використання модифікатора # і динамічного модифікатора


мінімальної ширини поля і точності

#include <stdio.h>

int main(void) {
int size, // мінімальна ширина поля
accuracy; // кількість цифр після коми
double value;
- 156 -

printf("%x %#x\n", 10, 10);

value = 123/3456;
size = 10;
accuracy = 4;
printf("%*.*f", size, accuracy, value);

return 0;
}

А ось відповідність для другого printf ():

printf("%*.*f", size, accuracy, 123.3456);


|| | |
'-+-----' |
| |
'----------------'
Ось яким буде виведення програми:

a 0xa
123.3456
9.4. Функція форматного введення з консолі scanf()
Функція scanf() - це функція введення символьної інформації з подальшим
перетворенням загального призначення, що виконує введення з консолі. Вона може
читати дані всіх вбудованих типів і автоматично перетворювати числа у відповідний
внутрішній формат. Функція scanf() багато в чому виглядає як зворотна до функції
printf(). Ось прототип функції scanf ():

int scanf (const char* керуючий_рядок, адреса_аргумента1, ...,


адреса_аргументаN);

Ця функція, після виконання, повертає у своєму імені кількість елементів даних, для
яких було успішно виконано операцію введення значення. У разі помилки scanf()
повертає EOF. Керуючий_рядок визначає кількість та вид перетворення зчитуваних
значень при записі їх в змінні, на які вказують елементи списку аргументів.
Керуючий рядок складається з символів трьох видів: специфікаторів
перетворення, розділювачів, символів, які не є розділювачами. Розглянемо їх
окремо.

Специфікатори перетворення

Кожен специфікатор формату введення (спеціфікатор перетворення)


починається зі знака % , причому специфікатори формату введення повідомляють
функції scanf() про тип зчитуваних даних. Перелік цих кодів (тобто літер-
специфікаторів) наведено в таблиці нижче. Специфікаторам перетворення в
порядку зліва направо ставляться у відповідність елементи списку аргументів.

Таблиця 9.3. Перелік специфікаторів перетворення функції введення scanf()


- 157 -

Код Значення
%c Читає поодинокий символ
%d Читає десяткове ціле число зі знаком
Читає ціле число як в десятковому, так і вісімковому або шістнадцятковому форматі
%i
зі знаком
%e Читає число з плаваючою точкою
%f Читає число з плаваючою точкою
%g Читає число з плаваючою точкою
%о Читає вісімкове число без знака
%s Читає рядок
%x Читає шістнадцяткове число без знака
%p Читає покажчик
%n Приймає ціле значення, що дорівнює кількості вже введених (лічених) символів
%u Читає десяткове ціле число без знака
%[] Читає набір сканованих символів
%% Читає знак процента

Введення чисел

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


перетворення %d або %i. Причому, за допомогою специфікатора %i можна вводити
цілі числа зі знаком, які представлено у вхідному рядку як у десятичній, так і у
восьмеричній та шістьнадцятирічній системах числення у відповідному вигляді,
наприклад:

16, 020, 0x10, 0X10.

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


експоненціальному вигляді, використовуються специфікатори перетворення %e, %f
або %g.
Функцію scanf() можна використовувати для читання цілих беззнакових значень
в вісімковій або шістнадцятковій системах числення, застосовуючи для цього
відповідно команди форматування %o і %x, остання з яких може бути як на
верхньому, так і на нижньому регістрі. Коли вводяться шістнадцятиричні числа, то
літери від А до F, що представляють шістнадцятиричні цифри, повинні бути на тому
ж самому регістрі, що й літера-специфікатор. Наступна програма читає вісімкове і
шістнадцяткове число:

Приклад 9.11. Приклад читання вісімкового і шістьнадцяткового числа

#include <stdio.h>
int main(void) {
int i, j;
scanf("%o%x", &i, &j);
printf("%o %x", i, j);
- 158 -
return 0;
}

Функція scanf() припиняє читання числа по спеціфікатору формата тоді, коли


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

Введення цілих значень без знака

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

unsigned num;
scanf("%u", &num);

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

Зчитування поодиноких символів з допомогою scanf()

Поодинокі символи можна прочитати за допомогою функції getchar() або будь-


якої функції, спорідненої з нею. Для тієї ж мети можна використовувати також виклик
функції scanf() зі специфікатором формату %c. Але, як і більшість реалізацій
getchar(), функція scanf() при використанні специфікатора перетворення %c
зазвичай буде виконувати порядково буферизоване введення, тобто вимагати
натиснення клавіші <ENTER> після натискання символу на клавіатурі. В
інтерактивному середовищі така ситуація викликає певні труднощі.
При читанні одиночного символу символи розділювачів читаються так само, як
і будь-який інший символ, хоча при читанні даних інших типів, розділювачі
інтерпретуються як роздільники полів. Наприклад, при введенні з вхідного потоку "x
y" фрагмент коду

scanf("%c%c%c", &a, &b, &c);

поміщає символ x в a, пробіл - в b, а символ y - в c.

Зчитування рядків

Для читання з вхідного потоку рядків можна використовувати функцію scanf() зі


специфікатором перетворення %s. Використання специфікатора перетворення %s
змушує scanf() читати символи до тих пір, поки не зустрінеться будь-який символ-
розділювач. Таким розділювачем може бути пробіл, роздільник рядків '\n', табуляція
'\t', вертикальна табуляція '\v', або подача сторінки '\f'. Зчитувані символи
розміщуються в символьному масиві, на який вказує відповідний аргумент, а після
введених символів ще додається символ кінця рядка '\0'. На відміну від gets(), яка
читає рядок, поки Ви не натиснете <ENTER>, scanf() читає рядок до тих пір, поки не
зустрінеться перший розділювач. Це означає, що scanf() не можна (без додаткових
модифікаторів) використовувати для читання рядка типу "це випробування", тому
що після пробілу процес читання припиниться. Щоб побачити, як діє специфікатор
%s, спробуємо при виконанні цієї програми ввести рядок "привіт всім":

Приклад 9.13. Робота зі специфікатором %s

#include <stdio.h>
- 159 -
int main(void) {
char str[80];
printf("Введіть рядок: ");
scanf("%s", str);
printf("Ось Ваш рядок: %s", str);
return 0;
}

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

Введення адреси

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


перетворення %p. Цей специфікатор перетворення змушує функцію scanf() читати
адресу в тому форматі, який визначено архітектурою центрального процесора.
Наприклад, наступна програма спочатку вводить адресу, а потім відображає те, що
знаходиться в пам'яті за цією адресою:

Приклад 9.13. Введення адреси в покажчик

/* Ця програма виконається з помилкою! */


#include <stdio.h>
int main(void) {
char *p;
printf("Введіть адресу: ");
scanf("%p", &p);
printf("За адресою %p знаходиться %c\n", p, *p);
return 0;
}

Специфікатор %n

Специфікатор %n вказує, що scanf() повинна помістити кількість символів,


введених до того моменту, коли зустрівся %n, з вхідного потоку в цілу змінну,
зазначену відповідним аргументом. Цей специфікатор використовується у деяких
випадках, пов'язаних з необхідністю підрахунка числа введених символів.

Приклад 9.14. Введення зі специфікатором %n

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <windows.h>

int main(void) {
int count;
double d;

SetConsoleCP(1251);
SetConsoleOutputCP(1251);

printf("\nВведіть якесь число: ");


scanf("%lg%n", &d, &count);
- 160 -
printf("%d\n", count);
return 0;
}

У результаті на екрані отримаємо:

Введіть якесь число: +123.234556782543E-12


21

Використання набору сканованих символів

Функція scanf() підтримує специфікатор формату загального призначення,


названий набором сканованих символів scanset. Набір сканованих символів являє
собою множину символів. Коли scanf() вводить рядок з використанням такого
спеціфікатора, то вводить тільки ті символи з рядка, які входять до цієї множини
(набору сканованих символів). Зчитуванні символи будуть розміщуватися в рядку
символів, який вказаний аргументом, що відповідає набору сканованих символів.
Цей набір визначається наступним чином: всі ті символи, які належить сканувати,
розміщують у квадратних дужках. Безпосередньо перед відкритою квадратною
дужкою повинен знаходитися знак %. Наприклад, наступний набір сканованих
символів дає вказівку scanf() вводити тільки символи X, Y і Z з вхідного рядка:

%[XYZ]

Зверніть увагу на те, що при використанні набору сканованих символів []


вказувати ще й спеціфікатор рядка s не потрібно! При використанні набору
сканованих символів функція scanf() продовжує читати символи, розміщуючі їх у
відповідний рядок символів, поки не зустрінеться символ, що не входить в цей набір.
В цей момент заповнення рядка символів завершується, а функція scanf()
переходить до наступного специфікатора в керуючому рядку. По виконанні scanf() в
рядку символів буде знаходитись рядок, що складається з дозволених символів,
причому цей рядок буде закінчуватися символом '\0'. Наступний приклад ілюструє
сказане:

Приклад 9.15. Використання прямого набору сканованих символів

#include <stdio.h>
int main(void) {
int i;
char str[80], str2[80];
scanf("%d%[abcdefg]%s", &i, str, str2);
printf("%d %s %s", i, str, str2);
return 0;
}

Вводимо 123abcdtye, а потім натискаємо клавішу <ENTER>. Після цього


програма виведе 123 abcd tye. Так як в даному випадку 't' не входить в набір
сканованих символів, то scanf() припинила читання символів в змінну str відразу
після того, як зустрівся символ 't'. Решта символів були розміщенні в змінній str2.
Крім того, можна вказати набір сканованих символів, що працює з точністю до
навпаки; тоді першим символом в такому наборі повинен бути ^. Цей символ дає
вказівку scanf() приймати будь-який символ, який не входить в набір сканованих
- 161 -
символів. Приклад %[^XYZ] – вказівка вводити усі символи з вхідного рядка, за
виключенням символів X, Y, Z.
У більшості реалізацій для вказівки діапазону символів можна використовувати
дефіс. Наприклад, вказаний нижче набір сканованих символів дає функції scanf()
вказівку приймати символи від А до Z:

%[A-Z]

Увага! У разі використання кирилиці, діапазон символів може і не працювати


(залежить від розташування кирилиці в кодовій сторінці операційної системи)!
Слід звернути увагу на такий важливий момент: набір сканованих символів
чутливий до регістру літер. Якщо потрібно сканувати літери і на верхньому, і на
нижньому регістрі, то їх треба вказувати окремо для кожного регістру, наприклад
так:

%[XYZxyz]

Увага! Спеціфікатор набору сканованих символів може не працювати в


окремих компіляторах для літер кирилиці!

Функція scanf() при використанні специфікатора %s буде вводити зчитуваний


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

scanf ("%s", str);

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


виникає завдання ввести рядок цілком, до символу '\n', слід використовувати такий
вигляд функції scanf() з модифікатором набору сканованих символів такого
вигляду:

scanf ("%[^\n]", str);

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

Пропуск зайвих розділювачів у вхідному рядку

Символ-розділювач у керуючому рядку дає scanf() вказівку пропустити в


потоці введення один або кілька початкових розділювачів. Такими розділювачами
можуть бути пробіл, роздільник рядків '\n', табуляція '\t', вертикальна табуляція '\v',
або подача сторінки '\f'. По суті, один символ-розділювач у керуючому рядку змушує
scanf() читати, але не зберігати будь-яку кількість (в тому числі і нульову)
розділювачів, які знаходяться перед першим символом, що вже не є розділювачем.

Символи у керуючому рядку, що не є розділювачами

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


функція scanf() прочитає символ з вхідного потоку, перевірить, чи збігається
прочитаний символ із зазначеним в керуючому рядку, і в разі збігу пропустить (без
введення!) прочитаний символ. Наприклад, "%d,%d" змушує scanf() прочитати ціле
значення, прочитати "кому" і пропустити її (якщо це була кома!). А потім вже тільки
прочитати наступне ціле значення. Якщо ж вказаний символ у вхідному потоці не
- 162 -
буде знайдений (наприклад, у данному випадку кома у вхідному потоці буде
відсутня), то scanf() завершиться з помилкою. Коли потрібно прочитати і відкинути
знак відсотка, то в керуючому рядку слід вказати %%.

Аргументи функції scanf()

Для всіх змінних, які повинні отримати значення за допомогою scanf(), повинні
бути вказані адреси. Це означає, що всі аргументи функції повинні бути
покажчиками. Згадаймо, що саме так в Сі створюється виклик за посиланням і саме
тоді функція може змінити вміст аргументу. Наприклад, для зчитування цілого
значення в змінну count можна використовувати такий виклик функції scanf():

scanf("%d", &count);

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

scanf("%s", str);

У цьому випадку саме ім'я str є покажчиком, і тому перед ним не потрібно ставити
оператор &.

Модифікатори формату

Як і printf(), функція scanf() дає можливість модифікувати деяке число своїх


специфікаторів формату. У специфікаторах формату для scanf() теж можливо
вказати модифікатор максимальної довжини поля. Це ціле число, розташоване між
% і специфікатором формату; воно обмежує число символів, що зчитуються з
вхідного поля у відповідний масив. Наприклад, щоб зчитувати в змінну str не більше
20 символів, пишемо

scanf("%20s", str);

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


функцій введення зчитування почнеться після того місця, де воно закінчилося при
попередньому виклику. Наприклад, якщо у відповідь на виклик scanf() з прикладу
вище ввести рядок

ABCDEFGHIJKLMNOPRSTUVWXYZ

то в str через специфікатор максимальної ширини поля буде розміщено тільки 20


символів, тобто символи до U включно. Це означає, що у вхідному буфері
залишилися ще не прочитаними символи VWXYZ. При наступному виклику scanf(),
наприклад при виконанні оператора

scanf("%s", str);

в str будуть розміщені літери UVWXYZ. Введення з поля може завершитися і до


того, як буде досягнута максимальна довжина поля - якщо зустрінеться символ-
розділювач. В такому випадку scanf() переходить до зчитування наступного поля у
вхідному рядку.
- 163 -
Щоб прочитати довге ціле, після знака % перед специфікатором формату слід
помістити l (ель). А для читання короткого цілого значення перед специфікатором
формату слід помістити n. Ці модифікатори можна використовувати з наступними
кодами форматів: d, i, o, u, x і n.
За замовчуванням специфікатори f, e та g дають scanf() вказівку присвоювати
дані змінній типу float. Якщо перед одним з цих специфікаторів буде поміщений l
(ель), то scanf() зможе присвоювати дані змінній типу double. Використання L дає
scanf() вказівку, що змінна, яка приймає дані, має тип long double.

Придушення введення

Функція scanf() може прочитати поле, але не присвоювати прочитане значення


ніякій змінній; для цього треба перед літерою-специфікатором формату поля
поставити зірочку *. Наприклад, коли виконується оператор

scanf("%d%*c%d", &x, &y);

можна ввести пару координат 10,10. Кома буде прочитана правильно, але нічому
не буде присвоєна. Придушення присвоєння особливо корисно тоді, коли потрібно
обробити тільки частину того, що вводиться.
9.5. Потоки і файли у мові Сі
Що таке потоки і файли і яким цілям служить одне і інше? В системі введення /
виведення мови Сі для програм підтримується єдиний інтерфейс, що не залежить
від того, до якого конкретного пристрою здійснюється доступ. Тобто в цій системі
між програмою і пристроєм знаходиться щось більш загальне, ніж сам пристрій.
Такий віртуальний логічний пристрій введення або виведення (пристрій більш
високого рівня абстракції) називається потоком (stream), в той час як конкретний
фізичний пристрій називається файлом. Важливо розуміти, яким чином
відбувається взаємодія потоків і файлів.

Потоки

Файлова система мови Сі призначена для роботи з різними пристроями, в


тому числі терміналами, дисководами, каналами зв'язку та іншими зовнішніми
пристроями. Навіть якщо якийсь пристрій сильно відрізняється від інших,
буферизована файлова система все одно представить його у вигляді логічного
пристрою, який називається потоком. Всі потоки поводяться схожим чином. І так
як вони в основному не залежать від фізичних пристроїв, то та ж функція, яка
виконує запис в дисковий файл, може ту ж операцію виконувати і на іншому
пристрої, наприклад, на консолі. Потоки бувають двох видів: текстові і двійкові
(бінарні).

Текстові потоки

Текстовий потік - це послідовність символів. У стандарті Сі вважається, що


текстовий потік організований у вигляді рядків, кожен з яких закінчується символом
нового рядка '\n'. Однак у кінці останнього рядка потоку цей символ не є
обов'язковим. Увага! У текстовому потоці, на вимогу базового середовища, можуть
відбуватися певні перетворення символів (не плутати з двійковими
перетвореннями!). Наприклад, символ нового рядка '\n' може бути замінений парою
- 164 -
символів - повернення каретки '\r' і переведення рядка '\n'. Тому може і не бути
однозначної відповідності між символами, які пишуться (читаються), і тими, які
зберігаються на зовнішньому пристрої. Крім того, кількість тих символів, які
пишуться (читаються), і тих, які зберігаються в зовнішньому пристрої, може також
не збігатися через можливі перетворення.

Двійкові потоки

Двійковий потік - це послідовність байтів, яка взаємно однозначно відповідає


послідовності байтів на зовнішньому пристрої, причому ніякого перетворення байтів
(символів) ніколи не відбувається. Крім того, кількість тих байтів, які пишуться
(читаються), і тих, які зберігаються на зовнішніх пристроях, однакова. Слід мати на
увазі, що у кінці двійкового потоку може додаватися визначена програмним
додатком додаткова кількість нульових байтів. Такі нульові байти, наприклад,
можуть використовуватися для заповнення вільного місця в блоці пам'яті
незначущою інформацією для того, щоб інформаційний сектор носія (512 байт для
NT та DOS систем) було повністю заповнено.

Файли

У мові Сі файлом може бути все що завгодно, починаючи з дискового файлу і


закінчуючи терміналом або принтером. Потік пов'язують з певним файлом,
виконуючи операцію відкриття. Як тільки файл відкритий, можна проводити обмін
інформацією між ним і програмою.
Але не у всіх файлів однакові можливості. Наприклад, до дискового файлу
прямий доступ можливий, в той час як до каналу зв'язку - ні. Таким чином, ми
прийшли до одного важливого принципу, що належить до системи введення /
виведення мови Сі: всі потоки однакові, а файли - ні.
Якщо файл може підтримувати запити на місце розташування (покажчик
поточної позиції), то при відкритті такого файлу покажчик поточної позиції у файлі
встановлюється на початок. При читанні з файлу (або запису в нього) кожного
символу покажчик поточної позиції збільшується, забезпечуючи тим самим
просування по файлу.
Файл від'єднується від певного потоку (тобто розривається зв'язок між файлом
і потоком) за допомогою операції закриття. При закритті файлу, відкритого з метою
виведення, вміст (якщо його ще не було повністю виведено) пов'язаного з ним
потоку записується на зовнішній пристрій. Цей процес, який зазвичай називають
примусовим скиданням (вмісту) буфера потоку, гарантує, що ніяка інформація
випадково не залишиться у буфері. Якщо програма завершує роботу нормально,
тобто або main() повертає керування операційній системі, або викликається
exit(), то всі файли закриваються автоматично з попереднім скиданням залишку
буферів у відповідні файли. У разі аварійного завершення роботи програми,
наприклад, в разі краху або завершення програми шляхом виклику стандартної
функції abort() (наприклад, при відлагоджуванні програми), файли не
закриваються. Вся інформація, яка залишилася в буферах файлів – щезає, що
часто-густо призводить як до псування самого файлу, так і файлової системи
ОС.
У кожного потоку, пов'язаного з файлом, є керуюча структура, яка містить
інформацію про файл. Ця структура називається дескриптором файла.
Дескриптор являє собою невеликий блок оперативної пам'яті, тимчасово
виділений операційною системою під зберігання інформації про файл, який був
відкритий програмою для використання. Покажчик файла - це покажчик на структуру
- 165 -
Cі (що називається дескриптором), і повертається бібліотечною функцією fopen().
Покажчик файлу використовується для ідентифікації файлу, обгортання
дескриптора файлу, функції буферизації і всіх інших функцій, необхідних для
операції введення-виведення. Покажчик файлу має тип FILE, визначення якого
можна знайти в "stdio.h". Це визначення може відрізнятися від одного компілятора
до іншого. Приклад одного такого визначення структури дескриптора наведено
нижче:

// A FILE Structure returned by fopen


typedef struct
{
unsigned char *_ptr;
int _cnt;
unsigned char *_base;
unsigned char *_bufendp;
short _flag;
short _file;
int __stdioid;
char *__newbase;
#ifdef _THREAD_SAFE
void *_lock;
#else
long _unused[1];
#endif
#ifdef __64BIT__
long _unused1[4];
#endif /* __64BIT__ */
} FILE;

У дескрипторі файлу простому програмісту ніколи не можна прямо нічого міняти, бо


це призведе до збою в роботі усієї програми! Часто у програмуванні
введення/виведення під дескриптором файлу розуміють саме покажчик на
дескриптор. Якщо це не призводить до хибного тлумачення це можна
використовувати у роботі.
Розмежування потоків і файлів може казатися на перший погляд зайвим або
навіть "зарозумілим". Однак треба пам'ятати, що основна мета такого
розмежування – забезпечити єдиний інтерфейс. Для виконання всіх операцій
введення / виведення слід використовувати тільки поняття потоків і застосовувати
лише одну файлову систему. Тоді процес введення або виведення від кожного
зовнішнього пристрою автоматично перетвориться системою введення / виведення
в легко керований потік.
9.6. Основні функції файлової системи Сі
Файлова система мови Сі складається з декількох взаємопов'язаних функцій.
Найпоширеніші з них показані в таблиці нижче. Для їх роботи до програми
обов'язково слід підключити заголовковий файл <stdio.h>.
- 166 -
Таблиця 9.4. Основні файлові функції в мові Сі

Ім’я Що виконує

fopen() Відкриває файл

fclose() Закриває файл

putc() Записує один символ (байт) до файла

fputc() Виконує ті самі дії, що і функція putc()

getc() Читає один символ (байт) з файлу

fgetc() Виконує ті самі дії, що і функція getc ()

fgets() Читає рядок з файлу до символа '\n'

fputs() Записує рядок в файл

Встановлює покажчик поточної позиції на певний байт у файлі (по


fseek()
номеру)

ftell() Повертає значення номера покажчика поточної позиції у файлі

fprintf() Виконує ті самі дії для файлу, що і функція printf() для консолі

Виконує ті самі дії по введенню для файлу, що і функція scanf() для


fscanf()
консолі

feof() Повертає значення true (істина), якщо досягнуто кінець файлу

Повертає значення false(хибне), якщо сталася помилка при операції


ferror()
введення / виведення

rewind() Встановлює покажчик поточної позиції на початок файла

remove() Видаляє файл на диску

fflush() Дозапис (скидання буфера) потока в файл

Заголовковий файл <stdio.h> надає усі прототипи функцій введення / виведення


і визначає наступні додаткові три типи даних: size_t, fpos_t і FILE. Типи size_t і
fpos_t являють собою різновиди такого типу, як ціле число без знака. А третій тип,
FILE, являє собою дескриптор файлу.
Покажчик на дескриптор файлу (часто його називають просто дескриптор файлу
і якщо це не вносить плутанини – так будемо називати його і ми) - це покажчик на
структуру типу FILE. Він вказує на структуру, яка містить різні відомості про файл,
- 167 -
наприклад, його ім'я, статус і значення покажчика поточної позиції у файлі. По суті,
дескриптор файлу визначає конкретний файл і використовується відповідним
потоком при виконанні функцій введення / виведення. Щоб мати змогу виконувати
файлові операції читання і запису, програми повинні використовувати дескриптори
відповідних файлів. Для оголошення дескриптора файлу, слід використовувати
наступний оператор:

FILE *fp;

Крім того, в <stdio.h> додатково визначається декілька макросів, такі як NULL,


EOF, FOPEN_MAX, SEEK_SET, SEEK_CUR і SEEK_END. Макрос NULL визначає
порожній (null) покажчик. Макрос EOF, часто визначається як -1, є значенням, що
повертається тоді, коли функція введення намагається виконати читання після кінця
файлу. FOPEN_MAX визначає ціле значення, що дорівнює максимально
дозволеному для однієї програми числа одночасно відкритих файлів. Інші макроси
використовуються разом з функцією fseek(), яка виконує операції прямого доступу
до файлу і будуть розглянуті пізніше.
9.7. Відкриття і закриття файлу
Функція fopen() відкриває потік і пов'язує з цим потоком певний файл. Потім вона
повертає дескриптор цього файлу. Найчастіше під файлом мається на увазі
дисковий файл. Прототип функції fopen () такий:

FILE* fopen(const char* ім’я_файла, const char* режим);

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


файлу, в яке також може входити специфікація шляху до цього файлу;
режим файлу – це рядок символів, який визначає, яким чином файл буде
відкритий.
У наступній таблиці показано, які значення рядка режим є допустимими. Рядки,
подібні до "r + b" можуть бути представлені і у вигляді "rb +".
Таблиця 9.5. Перелік всіх режимів відкриття файлу

Режим Що означає

r Відкрити текстовий файл для читання

w Створити текстовий файл для запису

a Додати в кінець текстового файлу

rb Відкрити двійковий файл для читання

wb Створити двійковий файл для запису


ab Додати в кінець двійкового файлу
r+ Відкрити текстовий файл для читання / запису

w+ Створити текстовий файл для читання / запису


- 168 -

Режим Що означає
Додати в кінець текстового файлу або створити текстовий файл для
a+
читання / запису
r+b Відкрити двійковий файл для читання / запису

w+b Створити двійковий файл для читання / запису


Додати в кінець двійкового файлу або створити бінарний файл для
a+b
читання / запису

Як уже згадувалося, функція fopen() повертає дескриптор файлу у вигляді


покажчика на тип FILE. Ніколи не слід змінювати значення цього покажчика в
програмі. Якщо при відкритті файлу відбувається помилка, то fopen() повертає
порожній (null) покажчик.

Зробимо пояснення до режимів в таблиці. Якщо спробувати відкрити файл лише


для читання, а він не існує, то робота fopen() завершиться відмовою. А якщо
спробувати відкрити файл в режимі дозапису, а сам цей файл не існує, то він просто
буде створений. Більш того, якщо файл відкритий в режимі дозапису, то все нові
дані, які записуються в нього, будуть додаватися в кінець файлу. Вміст, який
зберігався в ньому до відкриття (якщо тільки він був) змінено не буде. Далі, якщо
файл відкривають для запису, але з'ясовується, що він не існує, то він буде
створений. А якщо він існує, то вміст, який зберігався в ньому до відкриття, буде
втрачено, причому на його місці буде створений новий файл. Різниця між режимами
r+ і w+ полягає в тому, що якщо файл не існує, то в режимі відкриття r+ він створений
не буде, а в режимі w+ все відбудеться навпаки: файл буде створений! Більш того,
якщо файл вже існує, то відкриття його в режимі w+ призведе до втрати його вмісту,
а в режимі r+ він залишиться недоторканим.
З таблиці вище видно, що файл можна відкрити або в одному з текстових, або
в одному з двійкових режимів. У більшості реалізацій в текстових режимах кожна
комбінація кодів повернення каретки (ASCII 13) і кінця рядка (ASCII 10)
перетвориться при введенні в символ нового рядка. При виведенні ж відбувається
зворотній процес: символи нового рядка перетворюються в комбінацію кодів
повернення каретки (ASCII 13) і кінця рядка (ASCII 10). У двійкових режимах такі
перетворення не виконуються.
Максимальне число одночасно відкритих файлів визначається FOPEN_MAX. Це
значення не менш 8, але чому воно точно дорівнює - це повинно бути написано в
документації по компілятору.
Функція fclose() закриває потік, який був відкритий за допомогою виклику fopen().
Функція fclose() записує в файл всі дані, які ще залишалися в дисковому буфері, і
проводить, так би мовити, офіційне закриття файлу на рівні операційної системи.
Відмова при закритті потоку тягне всілякі неприємності, включаючи втрату даних,
зіпсовані файли і можливі періодичні помилки в програмі. Функція fclose() також
звільняє блок керування файлом, пов'язаний з цим потоком, даючи можливість
використовувати пам'ять під цей блок знову. Так як кількість одночасно відкритих
файлів обмежена, то, можливо, доведеться закривати один файл, перш ніж
відкривати інший. Прототип функції fclose () такий:

int fclose(FILE* дф);


- 169 -
де дф - дескриптор файлу, повернений в результаті виклику fopen(). Повернення
нуля означає успішну операцію закриття. У разі ж помилки повертається EOF. Щоб
точно дізнатися, в чому причина цієї помилки, можна використовувати стандартну
функцію ferror() (яка буде описана далі). Зазвичай відмова при виконанні fclose()
відбувається тільки тоді, коли носій був передчасно видалений з дисковода або на
носії не залишилося вільного місця.
У наступному прикладі функції fopen()/fclose() використовується для відкриття /
закриття існуючого текстового файлу test.txt для читання/запису.

Приклад 9.16 Використання функцій відкриття і закриття файла у парі

#define _CRT_SECURE_NO_WARNINGS
#include "stdlib.h"
#include "stdio.h"
#include "conio.h"
#include "math.h"
#include "locale.h"
#include "string.h"
#include "windows.h"

typedef struct student {


char FIO[50];
char gruppa[10];
int age;
int kurs;
} tStudent;

int main() {
FILE *fp;
char name[] = "test.txt";
tStudent buffer;
int err=0;
int rawFile;

SetConsoleCP(1251);
SetConsoleOutputCP(1251);

// відкриваємо файл
if (!(fp = fopen(name, "r+"))) {
printf("Не вдалося відкрити файл %s", name);
_getch();
return 1;
}

// якщо відкрити файл вдалося


rawFile = 1;
err = fscanf(fp, "%[^;];%[^;];%d;%d", buffer.FIO, buffer.gruppa, &buffer.age,
&buffer.kurs);

//err = fscanf(fp, "%[^;];", buffer.FIO);


//err = fscanf(fp, "%[^;];", buffer.gruppa);
//err = fscanf(fp, "%d;", &buffer.age);
- 170 -
//err = fscanf(fp, "%d", &buffer.kurs);

if (err != 4) {
printf("Помилка читання рядка %d файлу %s", rawFile, name);
_getch();
return 1;
}

// виконуємо потрібні дії над даними


buffer.age += 1;
buffer.kurs += 1;
rewind(fp);
fprintf(fp, "%s;%s;%d;%d", buffer.FIO, buffer.gruppa, buffer.age, buffer.kurs);

// закриваємо файл
if (fclose(fp)) {
printf("Не вдалося закрити файл %s", name);
_getch();
return 2;
}

_getch();
return 0;
}
Такий метод застосування функцій fopen() та fclose() допомагає при
відкритті/закритті файлу виявити будь-яку помилку, наприклад, захист носія / файла
від запису або нестачу місця на диску, видалення носія, причому виявити ще до
того, як програма спробує в цей файл що-небудь записати. Взагалі, завжди потрібно
спочатку отримати підтвердження, що функції fopen()/fclose() виконалися успішно, і
лише потім виконувати з файлом інші операції.
9.8. Запис та зчитування поодинокого символу (байту)
В системі введення / виведення мови Сі визначаються дві еквівалентні функції,
призначені для виведення поодиноких символів (байтів): putc() і fputc(). Дві ідентичні
функції є просто тому, щоб зберігати сумісність зі старими версіями Сі.
Функція putc() записує символи в файл, який за допомогою fopen() вже відкрито
в режимі запису. Прототип цієї функції є наступним:

int putc(int ch, FILE* дф);

де дф - це дескриптор файлу, повернений функцією fopen(),


ch – символ, що виводиться.
Дескриптор файлу повідомляє putc(), в який саме файл слід записувати символ.
Хоча ch і має тип int, проте записується тільки молодший байт слова.
Якщо функція putc() виконалась успішно, то вона повертає записаний символ.
Якщо виникла помилка - повертається EOF.
Щоб ввести символ також є дві еквівалентні функції: getc() і fgetc(). Обидві
існують для збереження сумісності зі старими версіями Сі. Функція getc() читає
символ з файлу, який за допомогою fopen() вже відкрито в режимі для читання.
Прототип у цієї функції наступний:

int getc(FILE* дф);


- 171 -

тут дф - це дескриптор файлу, який має тип FILE і який було повернуто функцією
fopen(). Функція getc() повертає ціле значення, але введений символ завжди
знаходиться в молодшому байті. Якщо не сталася помилка, то старші байти будуть
обнулені. Інакше (якщо сталася помилка читання чи досягнуто кінець файла) -
функція getc() повертає EOF. Тому, щоб прочитати символи до кінця текстового
файлу, можна користуватися наступним кодом:

do {
ch = getc(fp);
} while(ch!=EOF);

Однак слід мати на увазі, що getc() повертає EOF і у випадку помилки читання. Для
визначення того, що ж насправді сталося, можна використовувати функцію feof().

9.9. Використання функцій fopen(), getc(), putc(), та fclose()


Функції fopen(), getc(), putc() і fclose() - це мінімальний набір функцій для
операцій з файлами. Наступна програма на ймення KTOD, являє собою простий
приклад, в якому використовуються тільки функції putc(), fopen() і fclose(). У цій
програмі символи зчитуються з клавіатури і записуються в дисковий файл до тих
самих пір, поки користувач не введе знак долара. Файл задається в командному
рядку при запуску програми. Наприклад, якщо викликати програму KTOD, ввівши в
командному рядку KTOD TEST, то рядки тексту будуть вводитися в файл TEST.

Приклад 9.17. Програма введення з клавіатури на диск

#define _CRT_SECURE_NO_WARNINGS
#include "stdlib.h"
#include "stdio.h"
#include "conio.h"
#include "math.h"
#include "locale.h"
#include "string.h"
#include "windows.h"

int main(int argc, char* argv[]) {


FILE* fp;
char ch;

SetConsoleCP(1251);
SetConsoleOutputCP(1251);

if (argc != 2) {
printf("Ви забули ввести ім'я файлу.\n");
exit(1);
}

if (!(fp = fopen(argv[1], "w"))) {


printf("Помилка при відкритті файлу.\n");
exit(1);
}
- 172 -
do {
ch = getchar();
if (putc(ch, fp) == EOF) return 1;
} while (ch != '$');

if (fclose(fp))
return 2;

return 0;
}

Програма DTOS, що є доповненням до програми KTOD, читає будь-який


текстовий файл і виводить його вміст на екран.

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

#define _CRT_SECURE_NO_WARNINGS
#include "stdlib.h"
#include "stdio.h"
#include "conio.h"
#include "math.h"
#include "locale.h"
#include "string.h"
#include "windows.h"

int main(int argc, char* argv[]) {


FILE* fp;
char ch;

SetConsoleCP(1251);
SetConsoleOutputCP(1251);

if(argc!=2) {
printf("Ви забули ввести ім'я файлу.\n");
return 1;
}

if((fp=fopen(argv[1], "r"))==NULL) {
printf("Помилка при відкритті файлу.\n");
return 2;
}

ch = getc(fp); /* читання одного символа */

while (!feof(fp)) {
putc(ch, stdout); /* виведення на екран */
ch = getc(fp);
}

if (fclose(fp)) return 4;
- 173 -
return 0;
}

9.10. Використання feof()


Як вже говорилося, якщо досягнуто кінець файлу, то getc() повертає EOF. Однак
перевірка значення, повернутого getc(), не є найкращим способом дізнатися, чи
дійсно було досягнуто кінець файлу. По-перше, файлова система мови Сі може
працювати як з текстовими, так і з двійковими файлами. Коли файл відкривається
для двійкового введення, то може бути прочитано ціле значення, яке, як з'ясується
під час перевірки, якраз і буде дорівнювати EOF. В такому випадку програма
введення повідомить про те, що досягнут кінець файлу, чого насправді немає. По-
друге, функція getc() повертає EOF і в разі помилки читання, а не тільки тоді, коли
досягнуто кінець файлу. Якщо використовувати тільки те значення, що
повертається getc(), то неможливо визначити, що ж насправді сталося. Для
вирішення цієї проблеми в Сі є функція feof(), яка дійсно визначає, чи досягнуто
кінець файлу. Прототип функції feof() такий:

int feof(FILE* дф);

Якщо досягнуто кінця файлу, то feof() повертає true (істина); в іншому ж випадку
ця функція повертає нуль (false). Тому наступний код буде читати двійковий файл
до тих пір, поки дійсно не буде досягнутий кінець файлу:

while(!feof(fp)) ch = getc(fp);

Ясно, що цей метод можна застосовувати як до двійкових, так і до текстових файлів.


9.11. Введення / виведення рядків: функції fgets() та fputs()
Крім getc() і putc(), в мові Сі також підтримуються споріднені з ними функції
fgets() і fputs(). Перша з них читає рядки символів з файлу, а друга записує рядки
символів в файл. Ці функції працюють майже як putc() і getc(), але читають і
записують не один символ, а цілий рядок. Прототипи функцій fgets() і fputs() такі:

int fputs(const char* рядок, FILE* дф);


char* fgets(char* рядок, int довжина, FILE* дф);

Функція fputs() пише в потік дф рядок, на який вказує рядок. У разі помилки
запису ця функція повертає EOF.
Функція fgets() читає з певного потоку рядок, і робить це до тих пір, поки не буде
прочитаний символ нового рядка або кількість прочитаних символів не стане рівним
(довжина-1). Якщо був прочитаний роздільник рядків, він записується в рядок, чим
алгоритм роботи функції fgets() додатково відрізняється від функції gets().
Отриманий в результаті рядок буде закінчуватися символом кінця рядка '\0'. При
успішному завершенні роботи функція повертає покажчик на рядок, а у разі помилки
читання - порожній покажчик (null).

9.12. Функція rewind()


Функція rewind() встановлює покажчик поточної позиції у файлі на початок
файлу, вказаного як аргумент цієї функції. Іншими словами, функція rewind()
виконує "перемотування" файлу на початок. Ось її прототип:
- 174 -
void rewind(FILE* дф);

де дф – це дескриптор файлу.
Змінимо програму з п. 9.9 таким чином, щоб вона відображала вміст файлу
відразу після його створення. Щоб виконати відображення, програма після
завершення введення "перемотує" файл, а потім за допомогою функції fback() читає
його з самого початку. Зараз файл необхідно відкрити в режимі читання / запису,
використовуючи як аргумент, що задає режим відкриття, рядок "w+".

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
char str[82];
FILE* fp;
if (!(fp = fopen("TEST", "w+"))) {
printf("Помилка при відкритті файлу.\n");
exit(1);
}

do {
printf("Введіть рядок (порожній - для виходу):\n");
gets(str);
strcat(str, "\n"); /* додавання роздільника рядків */
fputs(str, fp);
} while(*str != '\n');

/* далі виконується зчитування та відображення введеного раніше файлу * /

rewind (fp); / * Встановити покажчик поточної позиції на початок файлу. * /


while(!feof(fp)) {
fgets(str, 81, fp);
printf(str);
}
return 0;
}

9.13. Функції ferror() и perror()


Функція ferror() визначає, чи дійсно сталася помилка під час виконання операції
з файлом. Прототип цієї функції наступний:

int ferror(FILE* дф);

де дф - дескриптор файлу. Вона повертає значення true (істини), якщо при


останній операції з файлом сталася помилка; в іншому ж випадку вона повертає
false (хибне). Увага! Так як при будь-якій операції з файлом встановлюється своя
умова помилки, то після кожної такої операції слід відразу викликати ferror(), а
інакше дані про помилку можуть бути втрачені.
Для опису і категоризації файлових помилок часто використовується функція
perror(). Її прототип має такий вигляд:

void perror(const char* str);


- 175 -

Функція perror() перетворює значення глобальної змінної errno, що містить код


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

Приклад:
Цей фрагмент видає повідомлення про будь-яку помилку введення / виведення,
яка може статися в потоці, пов'язаниму з дескриптором файла fp.

if ferror(fp) perror(" Помилка при роботі з файлом ");

У наступній програмі показано застосування ferror() і perror() при роботі в парі.


Програма видаляє табуляції з файлу, замінюючи їх відповідною кількістю пробілів.
Розмір табуляції визначається макросом TAB_SIZE. Слід звернути увагу на те, що
ferror() викликається після кожної операції з файлом. При запуску цієї програми в
командному рядку вказуються два параметри: імена вхідного і вихідного файлів.

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


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

#define _CRT_SECURE_NO_WARNINGS
#include "stdlib.h"
#include "stdio.h"
#include "conio.h"
#include "math.h"
#include "locale.h"
#include "string.h"
#include "windows.h"

#define TAB_SIZE 8

int main(int argc, char* argv[]) {


FILE *in, *out;
int tab, i;
char ch;

SetConsoleCP(1251);
SetConsoleOutputCP(1251);

if (argc != 3) {
printf("Синтаксис: detab <вхідний_файл> <вихідний файл>\n");
exit(1);
}

if ((in = fopen(argv[1], "rb")) == NULL) {


printf("Неможливо відкрити%s.\n", argv[1]);
exit(1);
}

if ((out = fopen(argv[2], "wb")) == NULL) {


- 176 -
printf("Неможливо відкрити%s.\n", argv[2]);
exit(1);
}

tab = 0;
do {
ch = getc(in);
if (ferror(in))
perror("Помилка при введенні символу ");

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


if(ch=='\t') {

for(i=tab; i<TAB_SIZE; i++) {


putc(' ', out);
if(ferror(out))
perror ("Помилка при виведенні символу ");
}
tab = 0;
}

else {

putc(ch, out);
if(ferror(out))
perror ("Помилка при виведенні символу ");
tab++;
if(tab==TAB_SIZE) tab = 0;
if(ch=='\n' || ch=='\r') tab = 0;
}
} while(!feof(in));
fclose(in);
fclose(out);

return 0;
}

9.14. Стирання файлів і дозапис потоків


Функція remove() видаляє вказаний файл. Ось її прототип:

int remove(const char* iм’я_файлу);

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


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

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

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
- 177 -

int main(int argc, char *argv[]) {


char answer;

if(argc!=2) {
printf("Синтаксис: xerase <iм’я_файлу>\n");
exit(1);
}

printf("Стерти %s? (Y/N): ", argv[1]);


answer = getche();

if(toupper(answer)=='Y')
if(remove(argv[1])) {
printf("Не можна стерти файл.\n");
exit(1);
}
return 0;
}

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


застосовується функція fflush(). Ось її прототип:

int fflush(FILE* дф);

Ця функція записує всі дані, що ще знаходяться в буфері в потік, який вказаний дф.
При виконанні функції fflush() з порожнім (null) дескриптором дф буде виконаний
дозапис змісту буфера в усі файли, відкриті для виведення та чищення вхідних
буферів усіх файлів відкритих на введення. Після свого успішного виконання fflush()
повертає нуль, в іншому випадку - EOF.

9.15. Функції fread() и fwrite()


Для читання і запису двійкових даних, розмір яких може займати більше 1 байта,
в файловій системі мови Сі є дві додаткові функції: fread() і fwrite(). Ці функції
дозволяють читати і записувати блоки даних довільного розміру будь-якого типу.
Прототипи цих функцій наступні:

size_t fread(void* буфер, size_t кількість_байтів, size_t лічильник, FILE* дф);


size_t fwrite(const void* буфер, size_t кількість_байтів, size_t лічильник,
FILE* дф);

Для fread() буфер - це покажчик на область пам'яті, в яку будуть прочитані дані
з потоку введення. А для fwrite() буфер - це покажчик на початок області пам'яті,
дані з якої будуть записані в потік виведення. Значення лічильник визначає, скільки
зчитується або записується елементів даних, причому довжина кожного елемента в
байтах дорівнює кількість_байт. (Згадаймо, що тип size_t визначається як один з
різновидів цілого типу без знака.) І, нарешті, дф - це дескриптор файлу.
Функція fread() повертає кількість прочитаних елементів. Якщо досягнуто кінець
файлу або сталася помилка, то повертається значення, що може бути меншим, ніж
значення лічильника. А функція fwrite() повертає кількість записаних елементів.
Якщо при цьому не сталося помилки, то повернений результат буде дорівнювати
значенню лічильника.
- 178 -
Як тільки файл відкритий для роботи з двійковими даними, fread() і fwrite()
відповідно можуть читати і записувати інформацію будь-якого типу. Наприклад,
наступна програма записує в дисковий файл дані типів double, int і short, a потім
читає ці дані з того ж файлу. Зверніть увагу, як в цій програмі при визначенні
довжини кожного типу даних використовується операція sizeof().

/ * Запис Не символьних даних в дисковий файл і подальше їх читання. * /

#include <stdio.h>
#include <stdlib.h>

#define SIZE_DB 100

int main(void) {
FILE *fp, *fpDB;
double d = 12.23;
short s = 101;
int i = 123023, size;
struct tDB {
char FIO[50];
double ZarPlata;
int Vik;
} Spivrob[SIZE_DB];

if (!(fp=fopen("test", "wb+"))) {
perror("Помилка при відкритті тестового файлу ");
exit(1);
}

if (!(fpDB=fopen("DB.bin", "wb+"))) {
perror("Помилка при відкритті файлу бази даних");
exit(2);
}

fwrite(&d, sizeof(double), 1, fp);


fwrite(&s, sizeof(short), 1, fp);
fwrite(&i, sizeof(int), 1, fp);

if ((size=fwrite(Spivrob, sizeof(tDB), SIZE_DB, fpDB)) != SIZE_DB) {


perror("Помилка при запису файлу бази даних");
exit (3);
}

rewind(fp);
rewind(fpDB);

fread(&d, sizeof(double), 1, fp);


fread(&s, sizeof(short), 1, fp);
fread(&i, sizeof(int), 1, fp);

if ((size=fread(Spivrob, sizeof(tDB), SIZE_DB, fpDB)) != SIZE_DB) {


perror("Помилка при читанні файлу бази даних");
- 179 -
exit (4);
}

printf("%f %d %d", d, i, s);

fclose(fp);
fclose(fpDB);
return 0;
}

Як видно з цієї програми, у якості буфера можна використовувати (і часто саме так
і роблять) просто пам'ять, в якій розміщена змінна. У цій простій програмі значення,
які повертаються функціями fread() і fwrite(), частково ігноруються. Однак на
практиці ці значення необхідно перевіряти, щоб виявити помилки.
9.16. Введення / виведеня при прямому доступі: функція fseek()
При прямому доступі можна виконувати операції введення / виведення,
використовуючи функцію fseek(), яка встановлює покажчик поточної позиції у файлі
на потрібну позицію (байт). Ось прототип цієї функції:

int fseek(FILE* дф, long int кількість_байтів, int початок_відліку);

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


fopen();
кількість_байтів - кількість байтів, рахуючи від початок_відліку, вона визначає
нове значення покажчика поточної позиції;
початок_відліку - це один з наступних макросів:
Початок файлу SEEK_SET
Поточна позиція SEEK_CUR
Кінець файлу SEEK_END
Тому, щоб отримати в файлі доступ на відстані кількість_байт байтів від
початку файлу, початок_відліку має бути SEEK_SET. Щоб при доступі відстань
відраховувалася від поточної позиції покажчика в файлі, використовується макрос
SEEK_CUR (у цьому випадку значення кількість_байтів може бути як додатним,
так і від'ємним числом). Щоб при доступі відстань відраховувалася від кінця файлу,
потрібно вказувати макрос SEEK_END. При успішному завершенні своєї роботи
функція fseek() повертає нуль, а в разі помилки - ненульове значення.
У наступній програмі показано, як використовується функція fseek(). Дана
програма в певному файлі відшукує деякий байт, а потім відображає його на екрані.
У командному рядку потрібно вказати ім'я вхідного файлу, а потім номер потрібного
байта, тобто його відстань в байтах від початку файлу.

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]) {


FILE* fp;

if (argc!=3) {
printf("Синтаксис: SEEK <ім’я_файлy> <номер байта>\n");
exit(1);
- 180 -
}

if (!(fp = fopen(argv[1], "rb"))) {


printf("Помилка при відкритті файлу.\n");
exit(1);
}

if (fseek(fp, atol(argv[2]), SEEK_SET)) {


printf("Seek error.\n");
exit(1);
}

printf ("В %d-м байті міститься %c.\n", atol(argv[2]), getc(fp));


fclose(fp);

return 0;
}

Поточне значення покажчика поточної позиції у файлі можна визначити за


допомогою функції ftell(). Ось її прототип:

long int ftell(FILE* дф);

Функція повертає значення покажчика поточної позиції у файлі, пов'язаному з


дескриптором файлу дф. При невдалому результаті вона повертає -1.
Зазвичай прямий доступ може знадобитися лише для двійкових файлів.
Причина тут проста – так як в текстових файлах можуть виконуватися перетворення
символів, то може і не бути прямої відповідності між тим, що знаходиться у файлі і
тим байтом, до якого потрібен доступ. Єдиний випадок, коли треба використовувати
fseek() для текстового файлу - це доступ до тієї позиції, яка була вже знайдена за
допомогою ftell(); такий доступ виконується за допомогою макросу SEEK_SET,
використовуваного в якості початку відліку.
Слід добре запам'ятати наступне: навіть якщо в файлі знаходиться один тільки
текст, все одно цей файл при необхідності можна відкрити і в двійковому режимі.
Ніякі обмеження, пов'язані з тим, що файли містять текст, для операцій прямого
доступу значення не мають. Ці обмеження стосуються тільки файлів, відкритих в
текстовому режимі.
9.17. Різновиди функцій prinf() і scanf()
Крім основних функцій введення / виведення, про які йшла мова, у стандартній
бібліотеці мови Сі також є функції fprintf(), sprint() і fscanf(), sscanf(). Ці чотири
функції, за винятком того, що призначені для роботи з вихідними / вхідними
файлами і рядками (замість термінала – клавіатури та екрана), поводяться точно
так, як і printf(), і scanf(). Прототипи функцій fprintf() і fscanf() наступні:

int fprintf(FILE* дф, const char* керуючий_рядок, arg1,..., argN);


int fscanf(FILE* дф, const char* керуючий_рядок, arg1,..., argM);

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


fopen(). Операції введення / виведення функцій fscanf() та fprintf() виконують з тим
файлом, на який вказує дф.
- 181 -
Попередження! Хоча читати різносортні дані з файлів на дисках і писати їх
у файли, розташовані також на дисках, часто легше за все саме за допомогою
функцій fprintf() і fscanf(), це не завжди найефективніший спосіб виконання операцій
читання і запису. Так як дані в форматі ASCII записуються так, як вони повинні
з'явитися на екрані (а не в двійковому вигляді), то кожен виклик цих функцій
пов'язаний з накладними витратами, пов'язаними з необхідністю перетворення в
/ із символьної форми в двійкову. Тому, якщо треба дбати про розмір файлу або
швидкість роботи, - то слід використовувати функції fread() і fwrite().
Прототипи функцій sprintf() і sscanf() наступні:

int sprintf(char* buf, const char* format, arg1,..., argN);


int sscanf(const char* buf, const char* format, arg1,..., argM);

Функція sprintf() ідентична функції printf() за винятком того, що потік виведення


записується у пам'ять, починаючи з адреси, на яку вказує покажчик buf, а не у
стандартний потік stdout. Після закінчення роботи функції цей масив буде
автоматично завершено символом кінця рядка '\n'.
Функція sscanf() ідентична функції scanf(), але дані читаються з масиву, що
адресується параметром buf, а не зі стандартного потоку введення stdin. Функцію
sscanf() дуже зручно використовувати спільно з функцією fscanf() при введенні полів
записів складних текстових структур. В цьому випадку першою виконується функція
fscanf(), що виконує читання в заданий рядок str одного запису структури даних з
текстового файлу по поточній структурі до обмежувача запису, а в другу чергу
виконується функція введення sscanf(), що читає вже з рядка str і вводить окремі
поля з рядка str в призначені для них поля відповідного структурного типу даних
програми.
9.18. Стандартні потоки і перенаправлення введення/виведення
На початку виконання кожної програми завжди автоматично відкриваються три
стандартних потоки. Це:
- stdin (стандартний поток введення);
- stdout (стандартний поток виведення);
- stderr (стандартний поток помилок).
Зазвичай, ці потоки направляються до консолі, але в середовищах, які
підтримують перенаправлення введення / виведення, вони можуть бути
перенаправлені операційною системою на інший пристрій. (Перенаправлення
підтримується, наприклад, такими операційними системами, як Windows, Linux,
UNIX.)
Так як імена стандартних потоків є одночасно і дескрипторами відповідних
файлів, то вони можуть використовуватися системою введення / виведення мови Сі
також для виконання операцій введення / виведення на консоль. Наприклад, stdin
використовується для зчитування з консолі, a stdout і stderr - для запису на консоль.
У ролі покажчиків файлів потоки stdin, stdout і stderr можна застосовувати в будь-
якій функції, де використовується змінна типу FILE*. Наприклад, для введення рядка
з консолі можна написати приблизно такий виклик fgets():

char str[255];
fgets(str, 80, stdin);

І дійсно, таке застосування fgets() може виявитися досить корисним. Як вже


говорилося раніше в лекціях, при використанні gets() не виключена можливість, що
масив, який використовується для прийому рядка, що вводиться користувачем,
- 182 -
буде переповнений. Це можливо тому, що gets() не проводить перевірку на
відсутність порушення границі масиву. Корисною альтернативою gets() є функція
fgets() з аргументом stdin, так як ця функція може обмежувати число зчитуваних
символів і таким чином не допустити переповнення масиву. Єдина проблема,
пов'язана з fgets(), полягає в тому, що вона не видаляє символ '\n' у кінці введеного
рядка (в той час як gets() видаляє!). Тому цей символ доводиться видаляти "вручну",
як показано в наступній програмі:

#include <stdio.h>
#include <string.h>

int main(void) {
char str[80];
int i;

printf("Введіть рядок: ");


fgets(str, 79, stdin);

/* видалити символ нового рядка, якщо він є */


i = strlen(str)-1;
if(str[i]=='\n') str[i] = '\0';

printf("Це Ваш рядок: %s", str);

return 0;
}

Не слід забувати, що stdin, stdout і stderr - це не змінні в звичайному сенсі, і їм


не можна присвоювати значення за допомогою fopen(). Крім того, саме тому, що на
початку роботи програми ці дескриптори файлів створюються автоматично, в кінці
роботи вони і закриваються автоматично. Таким чином, щоб уникнути помилок не
слід намагатися самостійно їх відкрити/закрити.
Для перенаправлення стандартних потоків в інше місце можна скористатися
функцією freopen(). Ця функція пов'язує наявний потік з новим файлом. Так що вона
цілком може зв'язати з новим файлом вже відкритий потік. Ось прототип цієї функції:

FILE* freopen(const char* ім’я_файла, const char* режим, FILE* потік);

де ім'я_файлу - це покажчик на нове ім'я файлу, який потрібно зв'язати з


потоком, на який вказує дескриптор потік. Файл відкривається в режимі режим; цей
параметр може приймати ті ж значення, що і відповідний параметр функції fopen().
Якщо функція freopen() виконалася успішно, то вона повертає дескриптор потік, а
якщо зустрілися помилки, - то NULL.
У наступній програмі показано використання функції freopen() для
перенаправлення стандартного потоку виведення stdout в файл з ім'ям OUTPUT.

#include <stdio.h>

int main(void) {
char str[80];

freopen("OUTPUT", "w", stdout);


- 183 -

printf("Введіть рядок: ");


gets(str);
printf(str);

return 0;
}

Взагалі, перенаправлення стандартних потоків за допомогою freopen() в деяких


випадках може бути корисним, наприклад, при відлагоджуванні програми. Однак
виконання дискових операцій введення / виведення на перенаправлених потоках
stdin і stdout не таке ефективне, як використання спеціалізованих функцій, таких як
fread() / fwrite() або fscanf() / fprintf().
- 184 -

ТЕМА 10. ПРЕПРОЦЕСОР

Препроцесор - це комп'ютерна програма, яка приймає на вході первісний текст


програми, що компілюється, відповідним чином змінює його і видає новий
перероблений текст програми, вже готовий для компіляції. Про текст на виході
препроцесора кажуть, що він знаходиться у формі, придатній для обробки
наступними програмами (компілятором). Результат і вид обробки залежать від виду
препроцесора; так, деякі препроцесори можуть тільки виконати просту текстову
підстановку, інші здатні за можливостями зрівнятися з мовами програмування.
Найчастіший випадок використання препроцесора - обробка вихідного коду перед
передачею його на наступний крок компіляції.
У вихідний код програми на мові Сі можна вставляти різні інструкції
препроцесора. Вони називаються директивами препроцесора і розширюють
можливості середовища програмування. Всі директиви препроцесора починаються
з символу # і повинні займати окремий рядок програми.
10.1. Директиви #define та #undef
Директива #define визначає ідентифікатор і послідовність символів, яка буде
підставлятися замість ідентифікатора кожен раз, коли він зустрінеться у вхідному
файлі. Ідентифікатор називається ім'ям макросу, а сам процес заміни -
макропідстановкою. У загальному вигляді директива виглядає таким чином:

#define ім'я _макроса послідовність _ символів

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


послідовністю символів послідовність_символів може бути будь-яка кількість
пробілів, але ознакою кінця послідовності символів може бути тільки роздільник
рядків.
Припустимо, наприклад, що замість значення 1 потрібно використовувати слово
LEFT (лівий), а замість значення 0 - слово RIGHT (правий). Тоді можна зробити
наступні оголошення за допомогою директиви #define:

#define LEFT 1
#define RIGHT 0

У результаті компілятор буде підставляти 1 або 0 кожен раз, коли в вашому файлі
вихідного коду зустрічається відповідно ідентифікатор LEFT або RIGHT. Наприклад,
наступний код виводить на екран 0 1 2:

printf("%d %d %d", RIGHT, LEFT, LEFT+1);

Після свого визначення ім'я макросу можна використовувати в визначеннях


інших імен макросів. Ось, наприклад, код, що визначає значення ONE (один), TWO
(два) і THREE (три):

#define ONE 1
#define TWO ONE+ONE
#define THREE ONE+TWO

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


ним послідовністю символів. Приклад: визначимо стандартне повідомлення про
помилку наступним чином
- 185 -

#define ERROR_MESSAGE "стандартна помилка при введенні \n"


/ * ... * /
printf(ERROR_MESSAGE);

Тепер кожен раз, коли зустрінеться ідентифікатор ERROR_MESSAGE, компілятор


буде його замінювати рядком "стандартна помилка при введенні \n". Для
компілятора оператор printf(ERROR_MESSAGE); насправді буде виглядати таким
чином:

printf("стандартна помилка при введенні \n");

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


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

#define XYZ це перевірка

printf("XYZ");

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


Якщо послідовність_символів не поміщається в одному рядку, то цю
послідовність можна продовжити в наступному рядку, помістивши в кінці
попереднього, як показано нижче, зворотну косу риску (слеш):

#define LONG_STRING "це дуже довга \


строка, використовувана у якості прикладу"

Програмісти, що пишуть програми на мові Сі, в іменах ідентифікаторів макросів


часто використовують літери верхнього регістру. Якщо розробники програм
дотримуються цього правила, то той, хто буде читати їхню програму, з першого
погляду зрозуміє, що буде відбуватися макрозаміна. Крім того, всі директиви #define
зазвичай прийнято розміщувати на самому початку файлу або в окремому
заголовку, а не розкидати по всій програмі.
Імена макросів часто використовуються для визначення імен константних
значень, що зустрічаються в програмі. Наприклад, є програма, в якій визначається
масив і кілька функцій, які отримують доступ до цього масиву. Замість того щоб
розмір масиву "зашивати" прямо в код у вигляді константи, цей розмір можна
визначити за допомогою оператора #define, а потім використовувати це ім'я макросу
всюди, де потрібен розмір масиву. Таким чином, якщо потрібно змінити цей розмір,
то потрібно змінити тільки відповідний оператор #define, і потім перекомпілювати
програму. Розглянемо, наприклад, наступний фрагмент програми

#define MAX_SIZE 100


/* ... */
float balance[MAX_SIZE];
/* ... */
for(i=0; i<MAX_SIZE; i++) printf("%f", balance[i]);
/* ... */
for(i=0; i<MAX_SIZE; i++) x =+ balance[i];

Розмір масиву balance визначається ім'ям макросу MAX_SIZE, і тому якщо цей
розмір буде потрібно в майбутньому змінити, то треба буде змінити тільки
- 186 -
визначення MAX_SIZE. В результаті при перекомпіляції програми всі звернення до
цього імені макроса, будуть автоматично змінені.

Визначення макросів з формальними параметрами

У директиви #define є ще одна велика перевага: ім'я макросу може визначатися


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

#include <stdio.h>

#define ABS(a) (a) < 0 ? -(a) : (a)

int main(void) {
printf("модулі чисел -1 і 1 дорівнюють відповідно %d і %d",
ABS(-1), ABS(1));
return 0;
}

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


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

ABS(10-20)

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

10-20 < 0 ? -10-20 : 10-20

що призведе до неправильного результату.


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

Директива #undef

Директива #undef видаляє раніше задане визначення імені макросу, тобто


"анулює" його визначення; саме ім'я макросу має бути після директиви. У
загальному вигляді директива #undef виглядає таким чином:

#undef ім'я _макросу

Ось як, наприклад, можна використовувати цю директиву:

#define LEN 100


#define WIDTH 100
- 187 -

char array[LEN][WIDTH];

#undef LEN
#undef WIDTH
/ * А тут і LEN і WIDTH вже не визначені * /

І LEN, і WIDTH визначені в програмі (і ними можна користуватися!), поки не зустрівся


відповідний оператор #undef. З цього місця програми відповідний макрос перестає
бути визначеним і ним користуватися вже не можна (при компіляції буде видано
помилку).
Директива #undef використовується в основному для того, щоб локалізувати
імена макросів лише в тих ділянках коду, де вони потрібні.
10.2. Директиви #error і #include
Директива #error змушує компілятор припинити компіляцію всієї програми в
місці, де ця директива зустрілася. Ця директива використовується в основному при
відлагоджуванні програми. У загальному вигляді директива #error виглядає таким
чином:

#еrrоr повідомлення-про-помилку

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


директива #error, то при компіляції програми виводиться повідомлення про помилку
- можливо, разом з іншою інформацією, яка визначається компілятором.
Директива #include дає вказівку компілятору вставити в цьому місці текст ще
одного вхідного файла - додатково до того файлу, в якому знаходиться сама ця
директива. Ім'я вхідного файлу має бути взятим у подвійні лапки або в кутові дужки.
Наприклад, обидві директиви

#include "stdio.h"
#include <stdio.h>

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


функцій системи введення / виведення.
Файли, імена яких знаходяться в директивах #include, можуть в свою чергу
містити інші директиви #include. Вони називаються вкладеними директивами
#include. Кількість допустимих рівнів вкладеності у різних компіляторів може бути
різним. Однак в стандарті С89 передбачено, що компілятори повинні допускати не
менше 8 таких рівнів. А в стандарті С99 передбачена підтримка не менше 15 рівнів
вкладеності.
Спосіб пошуку файлу залежить від того, чи взяте його ім'я в подвійні лапки або
ж в кутові дужки. Якщо ім'я взято в кутові дужки, то пошук файлу проводиться тим
способом, який визначений в компіляторі. Часто це означає пошук певного
системного каталогу, спеціально призначеного для зберігання таких файлів. Якщо
ім'я взяте в лапки, то пошук файлу проводиться іншим способом. У багатьох
компіляторах це означає початковий пошук файлу в поточному робочому каталозі.
Якщо ж файл в ньому не знайдено, то пошук повторюється вже в системних
каталогах так, ніби ім'я файлу взяте в кутові дужки.
Зазвичай, більшість програмістів імена стандартних заголовків файлів беруть в
кутові дужки. А використання лапок, зазвичай, приберігають для імен спеціально
розроблюваних ними файлів, що відносяться до конкретної програми.
- 188 -

10.3. Директиви умовної компіляції


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

Директиви #if, #else, #elif і #endif

Найпоширенішими директивами умовної компіляції є #if, #else, #elif і #endif. Вони


дають можливість в залежності від значення подальшого константного виразу
включати або виключати з компіляції ті або інші частини коду.
У загальному вигляді директива #if виглядає таким чином:

#if константний вираз


послідовність операторів
#endif

Якщо константний вираз, що знаходиться після #if, істинний, то компілюється код,


який знаходиться між цим виразом і директивою #endif. В іншому випадку цей код
пропускається. Директива #endif визначає кінець блоку #if, наприклад

#include <stdio.h>
#define MAX 100

int main(void) {

#if MAX>99
printf("Компілює для масиву, розмір якого більше 99.\n");
#endif

return 0;
}

Це програма виводить повідомлення на екран, тому що МАХ більше 99. Цей


приклад демонструє щось дуже важливе. Значення виразу, що знаходиться за
директивою #if, має бути обчисленим під час компіляції. Тому в цьому виразі
можуть перебувати тільки раніше визначені ідентифікатори і константи, – але
не змінні!
Директива #else працює в основному так само, як і else - ключове слово
оператора if мови Сі: задає альтернативу на той випадок, якщо не виконана умова
#if. Попередній приклад можна доповнити наступним чином:

#include <stdio.h>
#define MAX 10

int main(void) {

#if MAX>99
printf("Компілює код для масиву, розмір якого більше 99.\n");
#else
- 189 -
printf("Компілює код для невеликого масиву.\n");
#endif

return 0;
}

В цьому випадку з'ясовується, що МАХ менше 99, тому частина коду, що


відноситься до #if, не компілюється. Однак компілюється альтернативний код, що
відноситься до #else, і відкомпільована програма буде відображати повідомлення
"Компілює код для невеликого масиву".
Слід звернути увагу на те, що директива #else використовується для того, щоб
позначити і кінець блоку #if, і початок блоку #else. Це природно, оскільки будь-якій
директиві #if може відповідати тільки одна директива #endif.
Директива #elif означає "else if" і встановлюється для кількості варіантів
компіляції більше, ніж два (тобто, ланцюжок if-else-if. Після #elif знаходиться
константний вираз, так само, як і після #if. Якщо цей вираз істинний, то компілюється
блок коду, що знаходиться за ним, і більше не перевіряються ніякі інші вирази #elif.
В іншому ж випадку перевіряється наступний блок цієї послідовності. У загальному
вигляді #elif виглядає таким чином:

#if вираз1
послідовність операторів 1
#elif вираз2
послідовність операторів 2
#elif вираз3
послідовність операторів 3
#elif вираз4
послідовність операторів 4
#elif вираз5
.
.
.
#elif виразN
послідовність операторів N
#endif

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


використовується значення макросу ACTIVE_COUNTRY (для якої країни):

#define US 0
#define EN 1
#define FR 2
#define UA 3
#define RU 4
#define ACTIVE_COUNTRY UA

#if ACTIVE_COUNTRY == US
char currency[] = "долар";
#elif ACTIVE_COUNTRY == EN
char currency[] = "фунт";
#elif ACTIVE_COUNTRY == UA
char currency[] = "гривня";
- 190 -
#elif ACTIVE_COUNTRY == RU
char currency[] = "рубль";
#else
char currency[] = "євро";
#endif

Відповідно до стандарту С89 у директивах #if і #elif може бути не більше 8 рівнів
вкладеності. А відповідно до стандарту С99 програмістам дозволяється
використовувати не більше 63 рівнів вкладеності. При вкладеності кожна директива
#endif, #else або #elif відноситься до найближчої директиви #if або #elif (так само, як
і правило для умовного оператора if мови Сі). Наприклад, абсолютно правильним є
наступний фрагмент коду:

#if MAX>100
#if SERIAL_VERSION
int port=198;
#else
int port=200;
#endif
#else
char out_buffer[100];
#endif

Директиви #ifdef і #ifndef

Інший спосіб умовної компіляції - це використання директив #ifdef і #ifndef, які


відповідно означають "if defined" (якщо визначено) і "if not defined" (якщо не
визначено).
У загальному вигляді #ifdef виглядає таким чином:

#ifdef ім'я _макроса


послідовність операторів
#endif

Блок коду послідовність операторів компілюватиметься, якщо ім'я_макроса було


визначено раніше оператором препроцесору #define.
У загальному вигляді оператор #ifndef виглядає таким чином:

#ifndef ім'я _макроса


послідовність операторів
#endif

Блок коду послідовність операторів компілюватиметься, якщо ім'я_макроса ще не


було визначено жодним оператором препроцесора #define.
І в #ifdef, і в #ifndef можна використовувати оператор #else або #elif. Наприклад,

#include <stdio.h>

#define IVAN 10

int main(void) {
#ifdef IVAN
- 191 -
printf("Привіт, Ванюша\n");
#else
printf("Привіт, хто-небудь\n");
#endif
#ifndef ALLA
printf("А Аллочка ще не визначена!\n");
#endif

return 0;
}

виведе "Привіт, Ванюша", а також "А Аллочка ще не визначена!"

Використання defined

Крім застосування #ifdef, є ще один спосіб дізнатися, чи визначено в поточному


місці задане ім'я макросу. Можна використовувати директиву #if в поєднанні з
оператором часу компіляції defined. У загальному вигляді оператор defined виглядає
таким чином:

defined ім'я _макросу

Якщо ім'я _макросу визначено, то значення виразу препроцесора буде істинним; в


іншому випадку - хибним. Наприклад, щоб дізнатися, чи визначено ім'я макросу
MYFILE, можна використовувати одну з двох команд препроцесора:

#if defined MYFILE

або

#ifdef MYFILE

Можна також задати протилежну умову, поставивши ! прямо перед defined.


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

#if !defined DEBUG


printf("Кінцева версія!\n");
#endif

Єдина причина, з якої використовується оператор defined, полягає в тому, що з


його допомогою в #elif можна дізнатися, чи визначено ім'я макросу.

10.4. Директиви #line, #pragma і оператори препроцесора # і ##


Директива #line змінює вміст __LINE__ і __FILE__, які є зарезервованими
ідентифікаторами в компіляторі. У першому з них міститься номер компільованого
в даний момент рядка коду. А другий ідентифікатор - це рядок, що містить ім'я
компільованого в даний момент вхідного файлу. У загальному вигляді директива
#line виглядає таким чином:

#line номер "ім’я_файла"


- 192 -
де номер — це додатнє ціле число, яке стає новим значенням __LINE__, а
необов'язкове ім'я_файла - це будь-який допустимий ідентифікатор файлу, що стає
новим значенням __FILE__. Директива #line в основному використовується при
відлагоджуванні і у спеціальних застосуваннях.
Наприклад, наступний код визначає, що лічильник рядків буде починатися зі
100, а функція printf() виводить номер 101, тому що він розташований в другому
рядку програми після оператора препроцесора #line 100:

#include <stdio.h>
#line 100 / * Встановити лічильник рядків на рядок 100*/
int main(void) { /* рядок 100 */
printf("%d\n",__LINE__); /* рядок 101 */
return 0;
}

Директива #pragma - це обумовлена реалізацією компілятора директива, яка


дозволяє передавати компілятору різні вказівки. Наприклад, компілятор може
підтримувати трасування виконання програми. Тоді можливість трасування можна
вказувати в операторі #pragma. Можливості цієї директиви і пов'язані з нею
подробиці повинні бути описані в документації по конкретному компілятору.
Є ще два оператора препроцесора: # і ##. Вони застосовуються в поєднанні з
оператором #define.
Оператор #, який зазвичай називають оператором перетворення в рядок,
перетворює аргумент, перед яким стоїть, в рядок, взятий в лапки. Розглянемо,
наприклад, наступну програму:

#include <stdio.h>
#define mkstr(s) #s
int main(void) {
printf(mkstr(Мені подобається Cі));
return 0;
}

Препроцесор перетворює рядок

printf(mkstr(Мені подобається Cі));

в рядок

printf("Мені подобається Cі");

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


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

#include <stdio.h>
#define concat(a, b) a ## b
int main(void) {
int xy = 10;
printf("%d", concat(x, y));
return 0;
}
- 193 -
Препроцесор перетворить рядок

printf("%d", concat(x, y));

в рядок

printf("%d", xy);

10.5. Імена передумовлених макрокоманд


У мові Сі визначені п'ять вбудованих зумовлених імен макрокоманд. Ось вони:
__LINE__
__FILE__
__DATE__
__TIME__
__STDC__
У такій же послідовності про них тут і піде мова.
Про імена макросів __LINE__ і __FILE__ розповідалося, коли йшлося про
директиву #line. Кажучи коротко, вони містять відповідно номер рядка і ім'я файлу
компільованої програми.
В імені макросу __DATE__ міститься рядок у вигляді місяць/день/рік, тобто дата
компіляції вихідного коду в об'єктний.
В імені макросу __TIME__ міститься час компіляції програми. Це час , який
представлено рядком, що має вигляд «година:хвилина:cекунда:».
Якщо __STDC__ визначено як 1, то тоді компілятор виконує компіляцію
відповідно до стандарту Сі.
- 194 -

СПИСОК ЛІТЕРАТУРИ

Базова література

1. Шилдт Г. Полный справочник по С++, 4-е изд. – Москва: Вильямс, 2006. - 791 с.
2. Шилдт Г. С++. Базовый курс, 3-е издание. – Москва: Вильямс, 2010. – 624 с.
3. Шпак З.Я. Програмування мовою С. – Львів: Оріяна-Нова, 2006. – 432 с.
4. Рендольф Ник и др. Visual Studio 2010 для профессионалов.: Пер. с англ. — М.:
ООО “И.Д. Вильямс”, 2011. — 1184 с.
5. Голуб А. И. Правила программирования на C и C++ (2-е издание, 2001)
6. Столлман Р. Стандарт кодирования GNU, 1994. Источник
http://www.opennet.ru/docs/RUS/coding_standard/
7. ДСТУ 3008-2015. ЗВІТИ У СФЕРІ НАУКИ І ТЕХНІКИ. Структура і правила
оформлювання (2015).
8. ГОСТ 19.701-90 "ЕСПД. Схемы алгоритмов, программ, данных и систем. Условные
обозначения и правила выполнения".

Додаткова література

9. Джосаттис Н.М. Стандартная библиотека C++: справочное руководство, 2-е изд. –


Москва: ООО "И.Д. Вильямс", 2014. – 1136 с.
10. Прата С. Язык программирования С. Лекции и упражнения, 6-е изд. : Пер. с англ.
— М. : ООО “И.Д. Вильямс”, 2015. — 928 с.
11. Дейтел П.Дж., Дейтел Х.М. Как программировать на С++, 5-е издание. – Москва:
ООО "Бином-пресс", 2008. – 1456 с.
12. Дэвис С.Р. C++ для чайников, 4-е изд. – Москва: Издательский дом "Вильямс",
2003. – 336 с.
13. Саммит С. Язык С в вопросах и ответах, 1995
14. Страуструп Б. Программирование. Принципы и практика с использованием C++, 2-
е издание. – Москва: ООО "И.Д. Вильямс". 2016. – 1328 с.

Додаткова література , написана українською

15. Войтенко В.В., Морозов А.В. - C, C++. Teopiя та практика, видання 2. – Житомир:
ЖДТУ, 2004. – 324 с.
16. Вінник В.Ю. Алгоритмiчнi мови та основи програмування. Мова C. – Житомир:
ЖДТК, 2007. – 328 с.
17. Керніган Б., Річі Д. Мова програмування C, друге видання. – 232 с.
18. Щедріна О. І. Алгоритмізація та програмування процедур обробки інформації С++.
– Київ: КНЕУ, 2001. – 240 с.

You might also like