Professional Documents
Culture Documents
Пр1 КЛ (у) -2020
Пр1 КЛ (у) -2020
КОНСПЕКТ ЛЕКЦІЙ
Київ 2020
-2-
Відповідальний
редактор ______________________________
ЗМІСТ
Тема 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-
unsigned int 16 або 32 від 0 до 65535 (або від 0 до 4 294 967 295)
signed
unsigned
long
short
Базовий тип int може бути модифікованим кожним із цих специфікаторів. Тип
char модифікується з допомогою unsigned та signed, double — з допомогою long.
Для цілих можна використовувати специфікатор signed, але в цьому нема
необхідності тому, що при оголошенні цілого цей спеціфікатор встановлюється за
замовчуванням. Специфікатор signed частіше всього використовується для типу
char, який в певних реалізаціях за замовчуванням може бути беззнаковим.
Якщо специфікатор типу записати сам по собі (без наступного за ним базового
типу), передбачається, що він змінює int. Таким чином, наступні специфікатори
типів є еквівалентними:
Специфікатор Те ж саме
Signed signed int
Unsigned unsigned int
Long long int
Short short int
Правильні Неправильні
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;
Локальні змінні
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);
/* певні оператори внутрішнього блоку ... */
}
#include <stdio.h>
int main(void) {
int x;
x = 10;
if (x == 10) {
int x; /* ця x ховає зовнішній x */
x = 99;
printf("Внутрішня x: %d\n", x);
}
Глобальні змінні
#include <stdio.h>
int main(void) {
sp_to_dash("тестовий приклад");
return 0;
- 21 -
}
/ * Неправильний приклад. * /
void sp_to_dash(const char *str) {
while(*str) {
if (*str==' ' ) *str = '-'; /* це неправильно */
printf("%c", *str);
str++;
}
}
Кваліфікатор volatile
char ch = 'a';
int first = 0;
double balance = 123.23;
static
register
Специфікатор extern
#include "stdio.h"
int first = 10, last = 20;
int main(void) {
printf("%d %d \n", first, last);
return 0;
}
Буде надруковано 10 20
#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
.#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 -
Специфікатор static
#include "stdio.h"
int series(void) {
static int series_num = 100;
series_num = series_num + 23;
return series_num;
}
Глобальні статичні змінні
Специфікатор static в оголошенні глобальної змінної змушує компілятор
створити глобальну змінну, видиму тільки в тому файлі, в якому вона оголошена.
Статична глобальна змінна, таким чином, піддається внутрішньому зв'язуванню на
рівні файла, як описано раніше в пункті Специфікатор extern. Це означає, що хоч ця
змінна і глобальна, проте процедури в інших файлах не побачать її і не зможуть
випадково змінити її значення. Цим знижується ризик небажаних побічних ефектів.
Висновок: імена локальних статичних змінних видимі тільки всередині блоку, в
якому вони оголошені; імена глобальних статичних змінних видимі тільки
всередині файлу, в якому вони оголошені.
Специфікатор register
Рядкові константи
Код Призначення
\b Видалення останнього символа
\f Прокрутка до початку нової сторінки паперу чи екрану
\n Перехід каретки (курсора) на початок нового рядка
\r Повернення каретки (курсора) в початок рядка
\t Горизонтальна табуляція
\" Подвійні лапки
\' Одинарні лапки
\\ Зворотний слеш
\v Вертикальна табуляція
\a Сигнал (бі-іп)
\? Знак питання
\N Вісімкова константа (N - вісімкове представлення)
\xN Шістнадцяткова константа (N – шістнадцяткове представлення)
Оператор присвоєння
Оператор присвоєння може бути присутнім в будь-якому виразі мови Сі. Цим Сі
відрізняється від більшості інших мов програмування (Pascal, BASIC і FORTRAN), в
яких присвоєння можливо тільки в окремому операторі. Загальна форма оператора
присвоєння:
ім’я_змінної = вираз;
#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 = u; /* 3-й рядок */
printf("%u \n", ch);
i = f; /* 4-й рядок */
printf("%d \n", i);
f = i; /* 6-й рядок */
printf("%e \n", f);
return 0;
}
Множинні присвоєння
x = y = z = 0;
Складене присвоєння
x = x+10;
можно записати як
x += 10;
Арифметичні операції
Оператор Операція
- Віднімання, також унарний мінус
+ Додавання
* Множення
/ Ділення
% Залишок від ділення
-- Декремент, або зменшення
++ Інкремент, або збільшення
Оператор ділення по модулю % в Сі працює так само, як і в інших мовах, його
результатом є залишок від цілочисельного ділення. Цей оператор, проте, не можна
застосовувати до типів даних із плаваючою точкою.
Операції збільшення (інкремента) і зменшення (декремента)
У мові Сі є два корисних оператора, які значно спрощують широко поширені
операції. Це інкремент ++ і декремент --. Оператор ++ збільшує значення операнда
на 1, а -- зменшує на 1.
Як інкремент, так і декремент можуть передувати операнду (префіксна форма)
або слідувати за ним (постфіксна форма). Наприклад
x = x+1;
++x;
так і у вигляді
x++;
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. Символи операцій порівняння та логічних операцій
Оператори порівняння
Оператор Операція
> Більше ніж
>= Більше або дорівнює
< Менше ніж
<= Менше або дорівнює
== Дорівнює
!= Не дорівнює
Логічні операції
Оператор Операція
&& І
|| АБО
! НЕ, заперечення
Нижче наведено пріоритет логічних операцій:
Найвищий !
> >= < <=
== !=
&&
Найнижчий ||
Як і в арифметичних виразах, для зміни порядку виконання операцій порівняння
і логічних операцій можна використовувати круглі дужки.
Порозрядні операції
Оператор Операція
& І
| АБО
^ виключне АБО
~ НЕ (заперечення, доповнення до 1)
>> Зсув вправо
<< Зсув вліво
- 34 -
В автоматизації найбільш часто порозрядні операції застосовуються при
програмуванні перевірок стану пристроїв і механізмів, керування пристроями
шляхом включення / вимкнення пристроїв (шляхом зміни стану окремих біт портів
керування), програмуванні драйверів пристроїв, а також процедур, що виконують
операції над файлами.
В автоматизації часто буває необхідно перевірити один або кілька розрядів
інформаційних регістрів керуючих контролерів на предмет знаходження там
одиниць (скажімо, з метою перевірки стану клапана - відкритий / закритий). Для
цього використовується так звана маска, яка містить в розряді, що необхідно
перевірити - одиницю, а в усіх інших - нулі і порозрядна операція &. Наприклад, ми
хочемо перевірити, чи містить 3-й розряд змінної «а» одиницю, або нуль. Для цього,
наприклад, досить виконати наступний оператор:
Операція & призведе до того, що всі розряди змінної а, крім третього, будуть
скинуті в нуль. Отже, якщо у третьому розряді а буде стояти 1, то результат виразу
а&0x8 буде ІСТИНОЮ, в іншому випадку - ХИБНИМ.
Для встановлення певних розрядів числа (наприклад, якщо вони відповідають
за ввімкнення будь-яких пристроїв, наприклад двигунів) використовується операція
порозрядного логічного АБО | і знову маска. Розглянемо попередній приклад. Нехай
нам тепер треба встановити 1 в третьому розряді змінної а. Для цього достатньо
виконати наступну операцію з маскою
a=a|0x8;
/ * Інші біти в а залишаться незмінними! * /
Усі двійкові розряди змінної a при цьому залишаться без змін і лише третій розряд
буде встановлено в 1.
Порозрядні оператори зсуву >> і << зсувають всі біти змінної вправо або вліво.
Загальна форма оператора зсуву вправо:
змінна >> кількість_розрядів
Загальна форма зсуву вліво:
змінна << кількість_розрядів
Під час зсуву бітів в один кінець числа, інший кінець заповнюється нулями. Але
якщо число типу signed int від’ємне, то при зсуві вправо лівий кінець заповнюється
одиницями, так що знак числа зберігається. Порозрядні операції зсуву дуже корисні
при декодуванні виходів зовнішніх пристроїв, наприклад таких, як цифро-аналогові
перетворювачі, а також при зчитуванні інформації про статус пристроїв. Побітові
оператори зсуву можуть швидко множити і ділити цілі числа, за один такт, на два.
Порозрядна операція заперечення (доповнення) ~ інвертує стан кожного біта
операнда. Тобто, 0 перетворює в 1, а 1 - в 0.
Операція ?
x = 10;
y = x>9 ? 100 : 200;
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;
}
Порядок обчислень
х = f1() + f2();
- 37 -
немає ніяких гарантій того, що функція f1() буде викликана перед викликом f2().
Перетворення типів у виразах
Якщо у виразі зустрічаються змінні і константи різних типів, вони зводяться до
одного типу. Компілятор перетворює "менший" тип в "більший". Цей процес
називається просуванням типів. Спочатку всі змінні типів char і short int
автоматично просуваються в 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 -
(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;
}
x = y/3-34*temp+127;
x = (y/3) - (34*temp) + 127;
Оператор if
if (умовний_вираз)
оператор1;
else
оператор2;
Тут оператор може бути тільки одним оператором, блоком операторів або бути
відсутнім (порожній оператор). Фраза else може взагалі бути відсутньою.
if (x==3.45) ….
if (i) {
if (j) оператор1;
if (k) оператор2; /* цей if */
else оператор3; /* асоційований із цим else */
}
else оператор4; /* асоційований із if(i) */
/* вкладений if */
if(guess > magic)
printf("надто велике\n");
else
printf("надто мале\n");
}
return 0;
}
if (умова1)
оператор1;
else if (умова2)
оператор2;
else if (умова3)
оператор3;
.
.
.
else операторN;
x = 10;
y = x>9 ? 100 : 200;
x = 10;
if (x>9)
y = 100;
else
y = 200;
Умовний вираз
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 також може бути
відсутнім. В цьому випадку за відсутності збігів не виконується жоден оператор.
Згідно зі Стандартом С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 ("Не обрана жодна опція");
}
}
Оператор 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: <оператори>;
}
#include <stdio.h>
int main(void) {
int i;
{ /* блок операторів */
i = 120;
printf("%d", i);
}
return 0;
}
#include <stdio.h>
int main(void) {
int x;
for(x=1; x <= 100; x++)
printf("%d ", x);
return 0;
}
x = 10;
for (y=10; y!=x; ++y)
printf("%d", y);
printf("%d", y); /* Це єдиний printf()
котрий буде виконано */
цикл не виконається жодного разу, тому що при вході в цикл значення змінних х і у
рівні. Тому умова циклу приймає значення ХИБНОГО, а тіло циклу і оператор
інкремента не виконуються. Змінна у залишається рівною 10, єдиний результат
роботи цієї програми - вивід на екран числа 10 в результаті виклику функції printf(),
розташованої поза циклом.
- 49 -
/*
Використання операції "кома" в секціях заголовка циклу 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;
/* Інакше користувач допускається*/
.
.
.
}
#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;
}
Тут в main() кожна секція циклу for складається з викликів функцій, які
пропонують користувачеві ввести число і зчитують його. Якщо користувач ввів 0, то
цикл припиняється тому, що тоді умова циклу приймає значення ХИБНОГО. В
іншому випадку число зводиться до квадрату. Таким чином, в цьому прикладі циклу
for секції ініціалізації і збільшення використовуються незвично, але абсолютно
правильно.
Інша цікава особливість циклу for полягає в тому, що його секції можуть бути
взагалі порожніми, присутність в них будь-якого виразу не є обов'язковою. У
наступному прикладі цикл виконується, поки користувач не введе число 123:
for (x = 0; x! = 123;)
scanf ( "% d", &x);
Секція зміни параметру циклу for тут залишена порожньою. Це означає, що перед
кожною ітерацією значення змінної х перевіряється на відмінність від числа 123, а
збільшення не відбувається, воно тут непотрібне. Якщо з клавіатури ввести число
123, то умова приймає значення ХИБНОГО і програма виходить із циклу.
Ініціалізацію параметра циклу for можна зробити за межами цього циклу, але,
звичайно, до нього. Це особливо доречно, якщо початкове значення параметра
циклу обчислюється досить складно, наприклад:
Нескінченний цикл
for (;;)
printf ("Цей цикл повторюється нескінченно. \ n");
ch = '\0';
for( ; ; ) {
ch = getch(); /* зчитування символу */
if (ch=='A')
break; /* вихід із циклу */
}
printf("Ви надрукували 'A' “);
В данному прикладі цикл виконується до тих пір, поки користувач не введе з
клавіатуры символ 'А'.
Тут оператор (тіло циклу) може бути порожнім оператором, єдиним оператором або
блоком операторів. Умова (керуючий вираз) може бути будь-яким допустимим в
мові Сі виразом. Умова вважається істинною, якщо значення виразу не дорівнює
нулю, а оператор циклу виконується, якщо умова приймає значення ІСТИННОГО.
Якщо умова приймає значення ХИБНОГО, програма виходить з циклу і виконується
наступний за циклом оператор.
char wait_for_char(void)
{
char ch;
ch = '\0'; /* ініціалізація ch */
while(ch != 'A')
ch = getch();
return ch;
}
#include <stdio.h>
#include <string.h>
int main(void) {
char str[80];
strcpy(str, "це перевірка");
pad(str, 40);
printf("%d", strlen(str));
return 0;
}
while(working) {
working = process1();
if(working)
working = process2();
if(working)
working = process3();
- 55 -
}
}
do {
оператор;
} while (умова);
do {
scanf ("%d", &num);
} while (num > 100);
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 вираз;
Оператор goto
goto мітка;
частина програми 1
.
мітка:
частина програми 2
Мітка може знаходитися як до, так і після оператора goto. Наприклад,
використовуючи оператор goto, можна виконати цикл від 1 до 100 таким чином:
x = 1;
loop1:
x++;
if(x < =100)
goto loop1;
Оператор 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);
.
.
.
Функція 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 -
Як і інші змінні, масив повинен бути оголошений явно, щоб компілятор виділив для
нього певну область пам'яті (тобто розмістив масив). Тут тип позначає базовий тип
масиву, який є типом кожного елемента. Розмір задає кількість елементів масиву.
Наприклад, наступний оператор оголошує масив з 100 елементів типу double під
ім'ям balance:
char p [10];
#include <stdio.h>
int main(void) {
int x[100]; /* оголошення масиву 100 цілих */
int t;
return 0;
}
Під час виконання програми на Сі (на відміну від деяких інших мов
програмування) ніколи автоматично не перевіряється ні дотримання меж масивів,
ні їх вміст. В область пам'яті, зайняту масивом, може бути записано що завгодно,
навіть програмний код. Програміст повинен сам (де це необхідно) ввести
перевірку меж індексів.
Наступний приклад програми компілюється без помилок, однак при виконанні
відбувається порушення границі масиву count та руйнування сусідніх комірок
пам'яті:
int count[10], i;
/* тут порушена границя масиву count */
for(i=0; i<100; i++)
count[i] = i;
char a[7];
5.2. Рядки
Одновимірний масив найбільш часто застосовується у вигляді рядка символів.
Рядок - це одновимірний масив символів, що закінчується нульовим символом. У
мові Сі ознакою закінчення рядка (нульовим символом) служить символ '\0'. Таким
чином, рядок містить символи, що становлять рядок, а також кінцевий нульовий
символ. Це єдиний вид рядків, визначений в Сі.
Оголошуючи масив символів, призначений для зберігання рядка, необхідно
передбачити місце для нуля, тобто вказати його розмір в оголошенні на один
символ більше, ніж найбільша передбачувана кількість символів. Наприклад,
оголошення масиву str, призначеного для зберігання рядка з 10 символів, має
виглядати так:
char str[11];
#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;
}
Довжина: 5 5
Рядки рівні
Алло!Алло!
Перевірка,
л є в Алло
знайдено ив
- 63 -
d[1][2]
#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 1 2 3
--+-----------
0|1 2 3 4
1|5 6 7 8
2 | 9 10 11 12
кількість_байтів =
= розмір_1-го_виміру × розмір_2-го_виміру × sizeof(базовий_тип_елемента)
Масиви рядків
char str_array[4][21];
Щоб звернутися до окремого рядка масиву, потрібно вказати тільки лівий індекс.
Наприклад, виклик функції gets() з третім рядком масиву str_array як аргументом
можна записати так:
gets (str_array[2]);
gets (&str_array[2][0]);
Масиви, у яких число вимірів більше трьох, використовуються досить рідко, тому
що вони займають великий обсяг пам'яті. Наприклад, чотиривимірний масив
символів розмірністю 10x6x9x4 займає 2160 байтів. Якби масив містив
чотирибайтові цілі типу int, треба було б вже 8640 байтів. І обсяг необхідної пам'яті
з ростом числа вимірювань зростає експоненціально.
При зверненні до багатовимірних масивів комп'ютер багато часу витрачає на
обчислення адреси, так як при цьому доводиться враховувати значення кожного
індексу. Тому доступ до елементів багатовимірного масиву відбувається значно
повільніше, ніж до елементів одновимірного.
5.5. Ініціалізація масивів
У мові Сі масиви при оголошенні можна ініціалізувати. Загальна форма
ініціалізації масиву аналогічна ініціалізації змінної:
- 66 -
int sqrs[10][2] = {
{1, 1},
{2, 4},
{3, 9},
{4, 16},
{5, 25},
{6, 36},
{7, 49},
{8, 64},
{9, 81},
{10, 100}
};
Тут для задання розміру масиву довелося вручну підраховувати кількість символів
в кожному повідомленні. Однак в мові Сі є конструкція, завдяки якій компілятор
автоматично визначає необхідну довжину рядка. Якщо в операторі ініціалізації
масиву явно не вказано розмір масиву, компілятор створює масив такого розміру,
що в ньому вміщаються всі проініціалізовані елементи. Таким чином створюється
безрозмірний масив. Використовуючи цей метод, попередній приклад можна
записати так:
int sqrs[][2] = {
{1, 1},
{2, 4},
{3, 9},
{4, 16},
{5, 25},
{6, 36},
{7, 49},
{8, 64},
{9, 81},
{10, 100}
};
може виникнути ситуація, коли дві ділянки коду повинні працювати з одним і тим
самим фрагментом даних, а не з його окремими копіями.
- 70 -
тип *ім’я;
Тут тип - це базовий тип покажчика, ним може бути будь-який правильний тип. Ім'я
визначає ім'я змінної-покажчика.
Базовий тип покажчика визначає тип об'єкта, на який покажчик буде посилатися.
Фактично покажчик будь-якого типу може посилатися на будь-яке місце в пам'яті.
Однак виконувані з покажчиком операції істотно залежать від його типу. Наприклад,
якщо оголошено покажчик типу 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".
Присвоювання покажчиків
#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;
#include <stdio.h>
int main (void) {
float x = 100.1, y;
short int *p;
/* У наступному операторі покажчику на ціле p
присвоюється значення, що посилається на double. */
p = (short int*) &x;
return 0;
}
y = *р;
Адресна арифметика
p1++;
+------+
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;
Порівняння покажчиків
#include<stdio.h>
#include<stdlib.h>
#define SIZE 5
int pop(void);
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());
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);
}
return *p1 + 1;
char p[10];
p
&p[0]
вираз
p == &p[0]
a
&a[0][0]
Більш того, до елементу (0,4) можна звернутися двома способами: або вказавши
індекси масиву: а[0][4], або за допомогою покажчика: *((int*) а + 4). Аналогічно для
елемента (1,2): а[1][2] або *((int*) а + 12). У загальному вигляді для двовимірного
масиву справедлива наступна формула:
a[j][k] = *((базовий_тип*)а+(j*довжина_рядка)+k)
Приклад 6.3
int* x[10];
x[2] = &var;
В результаті цієї операції, такий вираз *х[2] має те саме значення, що і змінна var:
Масиви покажчиків часто використовуються при роботі з рядками. Наприклад,
можна написати функцію, що виводить потрібний рядок (розміщенний за індексом
num) з повідомленням про помилку:
Приклад 6.4
Масив err містить покажчики на рядки з повідомленнями про помилки. Тут рядкові
константи у виразі ініціалізації створюють покажчики на рядки. Аргументом функції
printf() служить один з покажчиків масиву err, який відповідно до індексу num вказує
на потрібний рядок з повідомленням про помилку. Наприклад, якщо в функцію
syntax_error() передається значення 2, то виводиться повідомлення "Помилка при
запису".
6.5. Багаторівнева адресація
Іноді покажчик може посилатися на покажчик, який вже сам посилається на
значення змінної. Це називається багаторівневою адресацією. Малюнок нижче
ілюструє концепцію багаторівневої адресації.
Покажчик Змінна
+--------+ +--------+
| Адреса |------->|Значення|
- 78 -
+--------+ +--------+
Однорівнева адресація
Багаторівнева адресація
float** newbalance;
Приклад 6.5
#include <stdio.h>
int main (void) {
int x, *p, **q;
x = 10;
p = &x;
q = &p;
printf ( "%d", **q); /* Друк значення x */
return 0;
}
char *p = 0;
p = NULL;
int *p = 0;
*р = 10; /* Помилка! */
Приклад 6.6
#include <stdio.h>
#include <string.h>
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;
char *p;
p = (char *) malloc (1000); / * Виділення 1000 байтів * /
- 82 -
int *p;
p = (int*) malloc (50 * sizeof (int));
Тут р - покажчик на ділянку пам'яті, виділений перед цим функцією malloc(). Функцію
free() ні в якому разі не можна викликати з неправильним аргументом (тобто,
аргументом, для якого не було раніше виділено пам'ять функцією malloc()). Це
миттєво зруйнує всю систему розподілу пам'яті і приведе до аваріного завершення
програми.
Підсистема динамічного розподілу пам'яті в Сі використовується спільно з
покажчиками для створення різних структур даних, таких, наприклад як динамічні
масиви.
Стандартом мови С89 визначені ще дві додаткові функції розподілу пам'яті,
що полегшують роботу з динамічними структурами. Перша з них - calloc().
Функція calloc() виділяє пам'ять, розмір якої дорівнює добутку num * size, тобто
пам'ять, достатню для розміщення масиву, що містить num об'єктів розміром size.
Всі байти виділеної пам'яті при цьому ініціалізуються нулями.
Функція calloc() повертає покажчик на перший байт виділеної області пам'яті.
Якщо для задоволення запиту немає достатнього обсягу неперервної пам'яті,
повертається нульовий покажчик. Перед спробою використовувати розподілену
пам'ять важливо перевірити, чи повернуте значення не дорівнює нулю. Слід
пам'ятати, що оскільки функція calloc() ініціалізує виділену область нулями -
- 83 -
продуктивність (швидкість роботи) її значно нижче, ніж у функції malloc(). Тому
використовувати її потрібно лише там, де вона дійсно потрібна.
Приклад 6.7
#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;
}
Тут 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>
Приклад 6.9
Приклад 6.10
#include <stdio.h>
#include <stdlib.h>
#define SIZE 10
int main(void) {
/* Оголошення покажчика на масив з SIZE рядків
в яких зберігаються цілі числа(int). */
int (*p)[SIZE];
register int i, j;
SetConsoleCP(1251);
SetConsoleOutputCP(1251);
if (!p) {
printf("Необхідна пам'ять не виділена. \n");
exit(1);
}
free(p);
- 86 -
return 0;
}
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
int (*p)[10];
p = x;
p = &x;
Приклад 6.11
/* Це програма з помилкою. */
#include <string.h>
#include <stdio.h>
int main(void) {
char *p1;
char s[80];
p1 = s;
do {
gets(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); /* зчитування рядка*/
return 0;
}
де:
тип_ДП - визначає тип даного, що повертається функцією, який також
називається результатом виконання функції. Функція може повертати будь-
який тип даних, за винятком агрегатних (масиви, структури і т.ін.);
список_формальних_параметрів - це список, елементи якого
відокремлюються один від одного комами. Кожен такий елемент
складається з імені змінної і її типу даних (наприклад, int iX, double pY). При
виклику функції формальні параметри набувають значення фактичних
аргументів, які передаються функції. Функція може бути і без формальних
параметрів (надалі, просто параметри), тоді їх список буде порожнім. Такий
порожній список можна вказати в явному вигляді, розмістивши всередині
дужок ключове слово void.
При оголошенні (деклараціях) змінних можна оголосити (декларувати) кілька
змінних одного і того ж типу, використовуючи для цього список одних тільки імен,
елементи якого відокремлені один від одного комами. На відміну від цього, всі
параметри функцій, навпаки, повинні оголошуватися окремо, причому для кожного
з них треба вказувати і тип, і ім'я. Тобто в загальному вигляді перелік оголошень
параметрів повинен виглядати наступним чином:
Функція is_in() має два параметри: s і d. Якщо символ 'c' входить в рядок s, то ця
функція повертає 1, в іншому випадку вона повертає 0.
Хоча параметри виконують спеціальне завдання, - приймають значення
аргументів, переданих функції, - вони все одно поводяться так, як і інші локальні
змінні. Формальним параметрам функції, наприклад, можна присвоювати будь-які
значення або використовувати ці параметри в будь-яких виразах в середині функції.
Виклики за значенням і за посиланням
У мовах програмування є два способи передачі значень підпрограмі (не
включаючи використання глобальних змінних!). Перший з них - виклик за
- 92 -
значенням. При його застосуванні у формальний параметр підпрограми копіюється
значення аргументу. У такому разі можливі зміни параметра викликаною
підпрограмою на аргумент не впливають.
Другим способом передачі аргументів підпрограмі є виклик за посиланням. При
його застосуванні в параметр копіюється адреса аргументу, тобто покажчик на
аргумент. Це означає, що, на відміну від виклику за значенням, зміни значення
параметра викликаною підпрограмою призводять до точно таких самих змін
значення самого аргументу, оскільки виконуються не на копії, а на самому аргументі.
Виклик за значенням
Приклад 7.01
#include <stdio.h>
int main(void) {
int t=10;
printf ("%d%d", sqr(t), t);
return 0;
}
Виклик за посиланням
Функція 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;
}
int main(void) {
int i[10];
func1(i);
/* ... */
}
Приклад 7.03
Звичайно, можна включити в оголошення і розмір 1-го виміру, але це зайве. Приклад
використання такої технології передачі двовимірного масива наведено нижче:
Приклад 7.4(1)
- 96 -
#include "stdlib.h"
#include "stdio.h"
#define SIZE1 10
#define SIZE2 10
int main(void) {
int x[SIZE1][SIZE2]; /* оголошення масиву SIZE1xSIZE2 цілих */
return 0;
}
Приклад 7.04(2)
#include "stdlib.h"
#include "stdio.h"
#include "conio.h"
#define SIZE1 10
#define SIZE2 10
int main(void) {
int x[SIZE1][SIZE2]; /* оголошення масиву SIZE1xSIZE2 цілих */
return 0;
}
cc назва_вихідного_файла_програми
Приклад 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 -
У багатьох середовищах всі аргументи командного рядка необхідно
відокремлювати один від одного пробілом або табуляцією. Коми, крапки з комою і
подібні символи тут розділювачами аргументів не вважаються. Наприклад,
Павло,Юля,Саша
char* argv[];
Приклад 7.06
if (argc<2) {
printf ("У командному рядку необхідно ввести число, з якого \ n ");
printf ("починається відлік. Спробуйте знову. \ n");
exit (1);
}
Приклад 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;
}
Всі функції, крім тих, які відносяться до типу void, повертають значення. Це
значення потрібно вказати виразом в операторі return. У функції, тип якої відмінний
від void, в операторі return необхідно обов'язково вказати значення, що
повертається. Тобто, якщо для будь-якої функції зазначено, що вона повертає
значення, то всередині цієї функції у будь-якого оператора return обов'язково має
бути свій вираз. Якщо функція, <тип> якої відмінний від void, виконується до самого
кінця (тобто до фігурної дужки, що її закриває) і відсутній оператор return зі
значенням <тип>, то повертається довільне (непередбачуване з точки зору
розробника програми!) значення. Хоча тут немає синтаксичної помилки, це є
серйозним упущенням і таких ситуацій необхідно уникати.
Якщо функція не оголошена як така, що має тип void, вона може
використовуватися як операнд у виразі. Тому кожен з наступних виразів є
правильним:
x = power(y);
if (max(x,y) > 100)
printf("більше");
for (ch=getch(); isdigit(ch); ) ... ;
Покажчики, що повертаються
Приклад 7.08
#include <stdio.h>
int main(void) {
char s[81], *p, ch;
#include <stdio.h>
void print_vertical(char* str); /* прототип */
Приклад 7.10
int main(void) {
int x;
x = 10;
sqr_it(x); /* невідповідність типів */
return 0;
}
float f (void);
- 104 -
Таким чином, компілятор дізнається, що у функції немає параметрів, і будь-яке
звернення до функції, в якому є аргументи, буде вважатися помилкою. У C++
використання ключового слова void всередині порожнього списку параметрів також
дозволено, але вважається зайвим.
Прототипи функцій дозволяють "відловлювати" помилки ще до запуску
програми. Крім того, вони забороняють виклик функції при розбіжності типів (тобто
з невідповідними аргументами) і тим самим допомагають перевіряти правильність
програми.
І наостанок, слід зазначити таке: так, як в ранніх версіях Сі синтаксис прототипів
в повному обсязі не підтримувався, то в Сі прототипи формально не обов'язкові.
Такий підхід необхідний для сумісності з Сі-кодом, створеним ще до появи
прототипів. Але якщо старий Сі-код переноситься в C++, то перед компіляцією цього
коду в нього необхідно додати повні прототипи функцій. Слід пам'ятати, що хоча
прототипи в Сі не є обов'язковими (для С89), але вони є обов'язковими для С99 і
подальших версій і C++. Це означає, що кожна функція в програмі на мові C++
повинна мати повний прототип. Тому при написанні програм на Сі в них вказуються
повні прототипи функцій – саме так чинить більшість програмістів, які працюють на
цій мові.
Будь-яка стандартна бібліотечна функція в програмі повинна мати прототип.
Тому для кожної такої функції необхідно ввести до програми відповідний файл
заголовку. Всі необхідні заголовки надаються компілятором Сі. В системі
програмування на мові Сі бібліотечними заголовками (зазвичай) є файли, в іменах
яких використовується розширення .h. У заголовку є два основних елементи: будь-
які визначення, що використовуються бібліотечними функціями, і прототипи
бібліотечних функцій. Наприклад, майже в усі програми курсу лекцій включається
файл <stdio.h>, тому що в цьому файлі знаходиться прототип для printf().
Приклад 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.12
#include "locale.h"
#include "string.h"
#include "windows.h"
SetConsoleCP(1251);
SetConsoleOutputCP(1251);
Усі чотири функції мають однакове ім’я 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)?
}
Слід відрізняти перевантаження функцій і редекларацію. Якщо функції з
однаковим ім’ям мають однакові параметри і типи результату, вважається, що друге
визначення розглядатиметься як редекларація (повторне визначення). Якщо
параметри функцій з однаковим ім’ям збігаються і функцій відрізняються лише
типом значення, яке повертається, це призведе до помилки.
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 ();
}
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
Ще приклад з printf():
з Прикладу 7.12 точка входу може мати такий вигляд (якщо виконати дізасемблер
на об'єктному модулі програми):
Приклад 7.15 Асемблерний код функції int Equal(char c1, char c2) з Прикладу 7.12
#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*); /* покажчик на функцію */
void check(char *a, char *b, int (*cmp)(const char *, const char *)) {
printf("Перевірка на збіг. \n");
if (!(*cmp)(a, b))
printf("Рівні");
else
printf("Не рівні");
}
(*сmp) (a, b)
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
char s1[80], s2[80];
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("Не рівні");
}
Якщо в цьому прикладі ввести перший символ першого рядка як цифру, то check()
використовує для порівняння функцію compvalues(), в іншому випадку - strcmp().
Функція check() викликає ту функцію, ім'я якої зазначено в списку аргументів при
виклику check(), тому вона в різних ситуаціях може викликати різні функції. Нижче
наведені результати роботи цієї програми в двох випадках:
«Я оглянулся посмотреть,
не оглянулась ли она,
- 113 -
чтоб посмотреть, не оглянулся ли я...»
(Максим .Леонидов, «Девочка-видение», песня).
Як видно, в main() викликається функція A(), яка викликає B(), а та в свою чергу
викликає знову A(). Зрозуміло, що ланцюжок функцій між викликами функції A()
може складатися не з однієї функції B(), а з більшої кількості різних функції.
Обидва вищевказаних приклади і літературно-художні приклади відносилися до
типу лінійної рекурсії. Разом з лінійної рекурсією, коли визначення об'єкта включає
в себе єдиний аналогічний об'єкт, існує ще й розгалужена (множинна) рекурсія,
коли таких об'єктів, що включаються, є декілька. Для рекурсивних функцій це
виглядає як два і більше окремих виклики функцією самої себе, або як рекурсивний
виклик функції в циклі.
/* Рекурсія, що розгалужується */
- 114 -
void F1(){
…if (…) F1(); /* Не більше 2 рекурсивних викликів */
…if (…) F1();
}
Хоча, як вже казали, посилання дуже схоже на покажчик, воно повинно бути
ініціалізоване не адресою об'єкта, а його значенням. Таким об'єктом, для якого
використовується посилання, може бути і покажчик:
refVal += 2;
int ii = refVal;
int* pi = &refVal;
int ival = 1024, ival2 = 2048; /* визначено два об'єкти типу int */
int &rval = ival, rval2 = ival2; /* визначене одна посилання і один об'єкт */
int ival3 = 1024, *pi = &ival3, &ri = ival3; /* визначено один об'єкт, один
покажчик на цей об'єкт і
одне посилання на цей же об'єкт*/
int &rval3 = ival3, &rval4 = ival2; /* визначені два посилання */
int *pi = 0;
int temp = 0;
const int &ri = temp;
змінна ival, на яку вказує pi, залишається незмінною, а pi отримує значення адреси
змінної ival2. І pi, і pi2 і тепер вказують на один і той же об'єкт ival2. Якщо ж ми
працюємо з посиланнями:
то саме значення ival змінюється, але посилання ri, як і раніше, адресує ival.
- 118 -
І виклик
swap(&I,&j);
відповідно.
- 119 -
struct addr {
char name[30];
char street[40];
char city[20];
unsigned int zip;
};
У цьому операторі оголошена змінна типу 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;
struct тег {
тип ім’я-члена;
тип ім’я-члена;
тип ім’я-члена;
.
.
.
- 121 -
} змінні-структури;
причому тег або змінні-структури можуть бути пропущені, але тільки не обидва
одночасно.
addr_info.zip = 02091;
ім’я-об’єкту.ім’я-члена
gets(addr_info.name);
Присвоювання структур
#include <stdio.h>
int main(void) {
struct {
int a;
int b;
} x, y;
x.a = 10;
printf("%d", y.a);
return 0;
}
printf("%d", addr_list[2].zip);
addr_list[2].name[0] = 'X';
struct addr {
char name[30];
char street[40];
char city[20];
unsigned int zip;
} addr_list[MAX];
int main(void) {
char choice;
SetConsoleCP(1251);
SetConsoleOutputCP(1251);
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;
}
/* Робота меню. */
int menu_select(void) {
char s;
int c;
do {
printf("\nВведіть номер потрібного пункту: ");
s = _getche();
} while (s<'1' || s>'4');
return s;
}
slot = find_free();
if (slot == -1) {
printf("\nСписок заповнений");
return -1;
}
Якщо всі елементи масиву структур зайняті, то find_free() повертає -1. Це зручне
число, тому що в масиві немає -1-го елемента.
Функція delete() пропонує користувачеві вказати індекс того запису з адресою,
який потрібно видалити. Потім функція для видалення лише обнуляє перший байт
поля name.
/* Видалення адреси */
int deleteInd (void) {
register int slot;
char s[81];
І остання функція, яка потрібна програмі, – це list(), яка виводить на екран весь
список розсилки:
При передачі члена структури до функції передається його значення, при цьому
не грає ролі те, що значення береться з члена структури. Приклад – наступна
структура і наступні передачі її членів у функцію:
struct fred {
char x;
int y;
float z;
char s[10];
} mike;
Оператор & стоїть безпосередньо перед ім'ям структури, а не перед ім'ям окремого
члена! І ще, оскільки s вже позначає адресу (найпершого елемента масиву
символів), оператор & не потрібен.
#include <stdio.h>
int main(void) {
struct struct_type arg;
arg.a = 1000;
f1(arg);
return 0;
}
#include <stdio.h>
int main(void) {
struct struct_type arg;
arg.a = 1000;
f1(arg); /* неспівпадіння типів */
return 0;
}
struct bal {
float balance;
char name[80];
} person;
p = &person;
p->balance
struct x {
int a[10][10]; /* масив 10 x 10 цілих значень */
float b;
} y;
y.a[3][7]
struct emp {
struct addr address; /* вкладена структура */
float wage;
} worker;
Тут структура була визначена як така, що має два члена. Першим членом є
структура типу addr, в якій знаходиться адреса працівника. Другий член - це wage,
де знаходяться дані про його зарплату. У наступному фрагменті коду елементу zip
з address присвоюється значення 93456.
- 130 -
worker.address.zip = 93456;
union тег {
тип ім’я-члена;
тип ім’я-члена;
тип ім’я-члена;
.
.
.
} змінні-цього-об’єднання;
Наприклад:
union pw {
short int i;
char ch[2];
};
union pw cnvt;
cnvt.i = 10;
#include <stdio.h>
#include <stdlib.h>
union pw {
short int i;
char ch[2];
};
int main(void) {
FILE *fp; /* вказівник на поток */
SetConsoleCP(1251);
SetConsoleOutputCP(1251);
- 132 -
fp = fopen("test.tmp", "wb+"); /* відкривається двійковий файл для читання і
запису */
if (!fp) {
printf("Файл не відкритий.\n");
exit(1);
}
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;
Тут тип означає тип бітового поля, а довжина - кількість біт, які займає це поле. Тип
бітового поля може бути int, signed або unsigned. Ніяким іншим типом бітове поле
описано бути не може.
Бітові поля довжиною 1 біт повинні оголошуватися як unsigned, оскільки 1 біт
не може мати знака. Бітові поля можуть мати довжину від 1 до16 біт для 16-бітних
середовищ і від 1 до 32 біт для 32-бітних середовищ.
До кожного бітового поля відбувається звернення за допомогою оператора
"крапка". Проте, якщо звернення до структури (об'єднання) відбувається за
допомогою покажчика, то слід використовувати оператор ->.
Бітові поля часто використовуються при аналізі даних, що надходять в
програму з обладнання. Наприклад, в результаті опитування стану адаптера
послідовного зв'язку може повертатися байт стану, організований в такий спосіб:
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("Дані готові");
status.ring = 0;
struct status_type {
unsigned: 4;
unsigned cts: 1;
unsigned dsr: 1;
} status;
struct emp {
struct addr address;
float pay;
unsigned lay_off: 1; / * Тимчасово звільнений або працює */
unsigned hourly: 1; /* погодинна оплата або оклад */
unsigned deductions: 3; /* процент податкових утримань * /
};
визначені дані про працівника, для яких виділяється тільки один байт, що містить
інформацію трьох видів: статус працівника, чи на окладі він, а також процент
утримань із його зарплати. Без бітового поля ця інформація займала б 3 байта.
#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, так як вказується довжина
поля в один біт. Якщо довжина поля більше одного біта, то поле може мати і
знаковий цілий тип.
#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
#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);
}
00001010
Тут тег і список змінних не є обов'язковими (але хоча б щось одне з них повинно
бути присутнім!). Наступний фрагмент коду визначає нумератор з ім'ям day_weekly
(день тижня):
- 138 -
day = Tuesday;
if (day == Sunday) printf ( "Сьогодні неділя. \n");
Щодо змінних нумерованого типу є одна поширена, але помилкова думка. Вона
полягає в тому, що їх елементи можна безпосередньо вводити і виводити. Це не
так. Наприклад, наступний фрагмент коду не виконуватиметься так, як того очікують
багато недосвідчених програмістів:
switch (day) {
case Monday: printf("Понеділок");
break;
case Tuesday: printf("Вівторок");
break;
і т.д.;
}
char name[][11] = {
"Понеділок",
"Вівторок",
"Середа",
"Четвер",
"П'ятниця",
"Субота",
"Неділя"
};
day = Wednesday;
printf("%s", name[day]);
#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"
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);
}
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;
struct s *p;
p = malloc(sizeof(struct s));
union u {
char ch;
int i;
double f;
} u_var;
Для нього sizeof(u_var) дорівнює 8. Втім, під час виконання не має значення, який
розмір насправді має u_var. Важливий розмір його найбільшого члена, так як будь-
яке об'єднання має бути такого ж розміру, як і його найбільший елемент.
8.10. Визначення нових типів з використанням ключового слова typedef
Нові імена типів даних в мові Сі можна визначати, використовуючи ключове
слово typedef. Насправді таким способом новий тип даних не створюється, а всього
лише визначається нове ім'я для вже існуючого типу.
Загальний вигляд декларації typedef (оператора typedef) такий:
де тип - це будь-який тип даних мови Сі, а нове_ім’я - нове ім'я цього типу. Нове
ім'я є доповненням до вже існуючого, а не його заміною.
Наприклад, для типу 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:
Тепер, усюди, де нам необхідно оголосити структурну змінну адреси досить буде
просто написати:
tAddr addr_list;
tAddr addr_list[MAX];
int getchar(void);
int putchar(int c);
int getch(void);
int getche(void);
#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.02. Програма читає рядок у масив str і виводить його довжину
#include <stdio.h>
#include <string.h>
int main(void) {
char str[80];
gets(str);
printf("Довжина в символах дорівнює %d", strlen(str));
- 147 -
return 0;
}
Функція puts() відображає на екрані свій строковий аргумент, після чого курсор
переходить на новий рядок. Прототип цієї функції має такий вигляд:
Функція Її дії
#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(void) {
char word[80], ch;
char **p;
SetConsoleCP(1251);
SetConsoleOutputCP(1251);
do {
puts("\nВведіть слово: ");
scanf("%s", word);
p = (char **)dic;
return 0;
}
Функція printf() після свого відпрацювання повертає у своєму імені число виведених
символів або негативне значення в разі помилки.
Перший параметр функції - керуючий_рядок, складається з елементів двох
видів. Перший з них - це символи, які належить вивести на екран; другий - це
специфікатори форматного перетворення, які визначають спосіб виведення
аргументів, що відповідають цим специфікаторам. Кожен такий специфікатор
починається зі знака процента, за яким слідує код формату.
Аргументів має бути рівно стільки, скільки і специфікаторів перетворення,
причому специфікатор і аргументи повинні попарно відповідати один одному в
напрямку зліва направо. Наприклад, в результаті такого виклику printf()
printf ( "Мені подобається мова%c %s", ‘C’, "і до того ж, дуже сильно!");
буде виведено
Код Формат
%c Символ
%d Десяткове представлення цілого зі знаком
Десяткове представлення цілого зі знаком. Для функції printf() дія обох
%i специфікаторів %d і %i повністю однакова. Відмінності існують лише для функції
scanf()
%e Експоненціальне представлення числа ('е' на нижньому регістрі)
%E Експоненціальне представлення числа ('Е' на верхньому регістрі)
%f Десяткове з плаваючою точкою
%g Залежно від того, який вивід буде коротшим, використовується %е або %f
%G Залежно від того, який вивід буде коротшим, використовується %Е або %f
%o Вісімкове представлення цілого без знака
%s Рядок символів
- 150 -
Код Формат
%u Десяткове представлення цілого без знака
%x Шістнадцяткове представлення цілого без знака (літери на нижньому регістрі)
%X Шістнадцяткове представлення цілого без знака (літери на верхньому регістрі)
Виводить представлення покажчика (форма представлення залежить від
%p
платформи!)
Аргумент, що відповідає цьому специфікатору, повинен бути покажчиком
на цілочисельну змінну. Специфікатор дозволяє зберегти в цій змінній
%n кількість записаних (вже виведених) символів поточною функцією printf()
(записаних до того місця, з якого починається запис в змінну, що відповідає
специфікатору %n).
%% Виводить знак %
Виведення символів
Виведення чисел
x.ddddd[e/E][+/-]yy
#include <stdio.h>
int main(void) {
double f;
for(f=1.0; f<1.0e+10; f=f*10)
printf("%g ", f);
return 0;
}
#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
#include <stdio.h>
int sample;
int main(void {
printf("%p", &sample);
return 0;
}
Специфікатор перетворення %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 в основному використовується в програмі для
виконання динамічного форматування при виведенні складних таблиць чи даних.
Модифікатори формату
#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
Модифікатори точності
#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
Це проста перев
#include <stdio.h>
int main(void) {
printf(".........................\n");
printf("по правому краю: %8d\n", 100);
printf(" по лівому краю: %-8d\n", 100);
return 0;
- 155 -
}
І ось що вийшло:
.........................
по правому краю: 100
по лівому краю: 100
Модифікатор * и #
#include <stdio.h>
int main(void) {
int size, // мінімальна ширина поля
accuracy; // кількість цифр після коми
double value;
- 156 -
value = 123/3456;
size = 10;
accuracy = 4;
printf("%*.*f", size, accuracy, value);
return 0;
}
a 0xa
123.3456
9.4. Функція форматного введення з консолі scanf()
Функція scanf() - це функція введення символьної інформації з подальшим
перетворенням загального призначення, що виконує введення з консолі. Вона може
читати дані всіх вбудованих типів і автоматично перетворювати числа у відповідний
внутрішній формат. Функція scanf() багато в чому виглядає як зворотна до функції
printf(). Ось прототип функції scanf ():
Ця функція, після виконання, повертає у своєму імені кількість елементів даних, для
яких було успішно виконано операцію введення значення. У разі помилки scanf()
повертає EOF. Керуючий_рядок визначає кількість та вид перетворення зчитуваних
значень при записі їх в змінні, на які вказують елементи списку аргументів.
Керуючий рядок складається з символів трьох видів: специфікаторів
перетворення, розділювачів, символів, які не є розділювачами. Розглянемо їх
окремо.
Специфікатори перетворення
Код Значення
%c Читає поодинокий символ
%d Читає десяткове ціле число зі знаком
Читає ціле число як в десятковому, так і вісімковому або шістнадцятковому форматі
%i
зі знаком
%e Читає число з плаваючою точкою
%f Читає число з плаваючою точкою
%g Читає число з плаваючою точкою
%о Читає вісімкове число без знака
%s Читає рядок
%x Читає шістнадцяткове число без знака
%p Читає покажчик
%n Приймає ціле значення, що дорівнює кількості вже введених (лічених) символів
%u Читає десяткове ціле число без знака
%[] Читає набір сканованих символів
%% Читає знак процента
Введення чисел
#include <stdio.h>
int main(void) {
int i, j;
scanf("%o%x", &i, &j);
printf("%o %x", i, j);
- 158 -
return 0;
}
Для введення цілого значення без знака у десятковій системі числення слід
використовувати специфікатор формату %u. Наприклад, оператори
unsigned num;
scanf("%u", &num);
виконують зчитування цілого числа без знака і присвоюють його змінній num.
Зчитування рядків
#include <stdio.h>
- 159 -
int main(void) {
char str[80];
printf("Введіть рядок: ");
scanf("%s", str);
printf("Ось Ваш рядок: %s", str);
return 0;
}
Введення адреси
Специфікатор %n
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <windows.h>
int main(void) {
int count;
double d;
SetConsoleCP(1251);
SetConsoleOutputCP(1251);
%[XYZ]
#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;
}
%[A-Z]
%[XYZxyz]
Для всіх змінних, які повинні отримати значення за допомогою scanf(), повинні
бути вказані адреси. Це означає, що всі аргументи функції повинні бути
покажчиками. Згадаймо, що саме так в Сі створюється виклик за посиланням і саме
тоді функція може змінити вміст аргументу. Наприклад, для зчитування цілого
значення в змінну count можна використовувати такий виклик функції scanf():
scanf("%d", &count);
Рядки будуть читатися в символьні масиви, а ім'я масиву без індексу є адресою
першого його елемента. Таким чином, щоб прочитати рядок в символьний масив,
можна використовувати оператор
scanf("%s", str);
У цьому випадку саме ім'я str є покажчиком, і тому перед ним не потрібно ставити
оператор &.
Модифікатори формату
scanf("%20s", str);
ABCDEFGHIJKLMNOPRSTUVWXYZ
scanf("%s", str);
Придушення введення
можна ввести пару координат 10,10. Кома буде прочитана правильно, але нічому
не буде присвоєна. Придушення присвоєння особливо корисно тоді, коли потрібно
обробити тільки частину того, що вводиться.
9.5. Потоки і файли у мові Сі
Що таке потоки і файли і яким цілям служить одне і інше? В системі введення /
виведення мови Сі для програм підтримується єдиний інтерфейс, що не залежить
від того, до якого конкретного пристрою здійснюється доступ. Тобто в цій системі
між програмою і пристроєм знаходиться щось більш загальне, ніж сам пристрій.
Такий віртуальний логічний пристрій введення або виведення (пристрій більш
високого рівня абстракції) називається потоком (stream), в той час як конкретний
фізичний пристрій називається файлом. Важливо розуміти, яким чином
відбувається взаємодія потоків і файлів.
Потоки
Текстові потоки
Двійкові потоки
Файли
Ім’я Що виконує
fprintf() Виконує ті самі дії для файлу, що і функція printf() для консолі
FILE *fp;
Режим Що означає
Режим Що означає
Додати в кінець текстового файлу або створити текстовий файл для
a+
читання / запису
r+b Відкрити двійковий файл для читання / запису
#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() {
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;
}
if (err != 4) {
printf("Помилка читання рядка %d файлу %s", rawFile, name);
_getch();
return 1;
}
// закриваємо файл
if (fclose(fp)) {
printf("Не вдалося закрити файл %s", name);
_getch();
return 2;
}
_getch();
return 0;
}
Такий метод застосування функцій fopen() та fclose() допомагає при
відкритті/закритті файлу виявити будь-яку помилку, наприклад, захист носія / файла
від запису або нестачу місця на диску, видалення носія, причому виявити ще до
того, як програма спробує в цей файл що-небудь записати. Взагалі, завжди потрібно
спочатку отримати підтвердження, що функції fopen()/fclose() виконалися успішно, і
лише потім виконувати з файлом інші операції.
9.8. Запис та зчитування поодинокого символу (байту)
В системі введення / виведення мови Сі визначаються дві еквівалентні функції,
призначені для виведення поодиноких символів (байтів): putc() і fputc(). Дві ідентичні
функції є просто тому, щоб зберігати сумісність зі старими версіями Сі.
Функція putc() записує символи в файл, який за допомогою fopen() вже відкрито
в режимі запису. Прототип цієї функції є наступним:
тут дф - це дескриптор файлу, який має тип FILE і який було повернуто функцією
fopen(). Функція getc() повертає ціле значення, але введений символ завжди
знаходиться в молодшому байті. Якщо не сталася помилка, то старші байти будуть
обнулені. Інакше (якщо сталася помилка читання чи досягнуто кінець файла) -
функція getc() повертає EOF. Тому, щоб прочитати символи до кінця текстового
файлу, можна користуватися наступним кодом:
do {
ch = getc(fp);
} while(ch!=EOF);
Однак слід мати на увазі, що getc() повертає EOF і у випадку помилки читання. Для
визначення того, що ж насправді сталося, можна використовувати функцію feof().
#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"
SetConsoleCP(1251);
SetConsoleOutputCP(1251);
if (argc != 2) {
printf("Ви забули ввести ім'я файлу.\n");
exit(1);
}
if (fclose(fp))
return 2;
return 0;
}
#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"
SetConsoleCP(1251);
SetConsoleOutputCP(1251);
if(argc!=2) {
printf("Ви забули ввести ім'я файлу.\n");
return 1;
}
if((fp=fopen(argv[1], "r"))==NULL) {
printf("Помилка при відкритті файлу.\n");
return 2;
}
while (!feof(fp)) {
putc(ch, stdout); /* виведення на екран */
ch = getc(fp);
}
if (fclose(fp)) return 4;
- 173 -
return 0;
}
Якщо досягнуто кінця файлу, то feof() повертає true (істина); в іншому ж випадку
ця функція повертає нуль (false). Тому наступний код буде читати двійковий файл
до тих пір, поки дійсно не буде досягнутий кінець файлу:
while(!feof(fp)) ch = getc(fp);
Функція fputs() пише в потік дф рядок, на який вказує рядок. У разі помилки
запису ця функція повертає EOF.
Функція fgets() читає з певного потоку рядок, і робить це до тих пір, поки не буде
прочитаний символ нового рядка або кількість прочитаних символів не стане рівним
(довжина-1). Якщо був прочитаний роздільник рядків, він записується в рядок, чим
алгоритм роботи функції fgets() додатково відрізняється від функції gets().
Отриманий в результаті рядок буде закінчуватися символом кінця рядка '\0'. При
успішному завершенні роботи функція повертає покажчик на рядок, а у разі помилки
читання - порожній покажчик (null).
де дф – це дескриптор файлу.
Змінимо програму з п. 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');
Приклад:
Цей фрагмент видає повідомлення про будь-яку помилку введення / виведення,
яка може статися в потоці, пов'язаниму з дескриптором файла fp.
#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
SetConsoleCP(1251);
SetConsoleOutputCP(1251);
if (argc != 3) {
printf("Синтаксис: detab <вхідний_файл> <вихідний файл>\n");
exit(1);
}
tab = 0;
do {
ch = getc(in);
if (ferror(in))
perror("Помилка при введенні символу ");
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;
}
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
- 177 -
if(argc!=2) {
printf("Синтаксис: xerase <iм’я_файлу>\n");
exit(1);
}
if(toupper(answer)=='Y')
if(remove(argv[1])) {
printf("Не можна стерти файл.\n");
exit(1);
}
return 0;
}
Ця функція записує всі дані, що ще знаходяться в буфері в потік, який вказаний дф.
При виконанні функції fflush() з порожнім (null) дескриптором дф буде виконаний
дозапис змісту буфера в усі файли, відкриті для виведення та чищення вхідних
буферів усіх файлів відкритих на введення. Після свого успішного виконання fflush()
повертає нуль, в іншому випадку - EOF.
Для fread() буфер - це покажчик на область пам'яті, в яку будуть прочитані дані
з потоку введення. А для fwrite() буфер - це покажчик на початок області пам'яті,
дані з якої будуть записані в потік виведення. Значення лічильник визначає, скільки
зчитується або записується елементів даних, причому довжина кожного елемента в
байтах дорівнює кількість_байт. (Згадаймо, що тип size_t визначається як один з
різновидів цілого типу без знака.) І, нарешті, дф - це дескриптор файлу.
Функція fread() повертає кількість прочитаних елементів. Якщо досягнуто кінець
файлу або сталася помилка, то повертається значення, що може бути меншим, ніж
значення лічильника. А функція fwrite() повертає кількість записаних елементів.
Якщо при цьому не сталося помилки, то повернений результат буде дорівнювати
значенню лічильника.
- 178 -
Як тільки файл відкритий для роботи з двійковими даними, fread() і fwrite()
відповідно можуть читати і записувати інформацію будь-якого типу. Наприклад,
наступна програма записує в дисковий файл дані типів double, int і short, a потім
читає ці дані з того ж файлу. Зверніть увагу, як в цій програмі при визначенні
довжини кожного типу даних використовується операція sizeof().
#include <stdio.h>
#include <stdlib.h>
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);
}
rewind(fp);
rewind(fpDB);
fclose(fp);
fclose(fpDB);
return 0;
}
Як видно з цієї програми, у якості буфера можна використовувати (і часто саме так
і роблять) просто пам'ять, в якій розміщена змінна. У цій простій програмі значення,
які повертаються функціями fread() і fwrite(), частково ігноруються. Однак на
практиці ці значення необхідно перевіряти, щоб виявити помилки.
9.16. Введення / виведеня при прямому доступі: функція fseek()
При прямому доступі можна виконувати операції введення / виведення,
використовуючи функцію fseek(), яка встановлює покажчик поточної позиції у файлі
на потрібну позицію (байт). Ось прототип цієї функції:
#include <stdio.h>
#include <stdlib.h>
if (argc!=3) {
printf("Синтаксис: SEEK <ім’я_файлy> <номер байта>\n");
exit(1);
- 180 -
}
return 0;
}
char str[255];
fgets(str, 80, stdin);
#include <stdio.h>
#include <string.h>
int main(void) {
char str[80];
int i;
return 0;
}
#include <stdio.h>
int main(void) {
char str[80];
return 0;
}
#define LEFT 1
#define RIGHT 0
У результаті компілятор буде підставляти 1 або 0 кожен раз, коли в вашому файлі
вихідного коду зустрічається відповідно ідентифікатор LEFT або RIGHT. Наприклад,
наступний код виводить на екран 0 1 2:
#define ONE 1
#define TWO ONE+ONE
#define THREE ONE+TWO
printf("XYZ");
Розмір масиву balance визначається ім'ям макросу MAX_SIZE, і тому якщо цей
розмір буде потрібно в майбутньому змінити, то треба буде змінити тільки
- 186 -
визначення MAX_SIZE. В результаті при перекомпіляції програми всі звернення до
цього імені макроса, будуть автоматично змінені.
#include <stdio.h>
int main(void) {
printf("модулі чисел -1 і 1 дорівнюють відповідно %d і %d",
ABS(-1), ABS(1));
return 0;
}
ABS(10-20)
Директива #undef
char array[LEN][WIDTH];
#undef LEN
#undef WIDTH
/ * А тут і LEN і WIDTH вже не визначені * /
#еrrоr повідомлення-про-помилку
#include "stdio.h"
#include <stdio.h>
#include <stdio.h>
#define MAX 100
int main(void) {
#if MAX>99
printf("Компілює для масиву, розмір якого більше 99.\n");
#endif
return 0;
}
#include <stdio.h>
#define MAX 10
int main(void) {
#if MAX>99
printf("Компілює код для масиву, розмір якого більше 99.\n");
#else
- 189 -
printf("Компілює код для невеликого масиву.\n");
#endif
return 0;
}
#if вираз1
послідовність операторів 1
#elif вираз2
послідовність операторів 2
#elif вираз3
послідовність операторів 3
#elif вираз4
послідовність операторів 4
#elif вираз5
.
.
.
#elif виразN
послідовність операторів N
#endif
#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
#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 MYFILE
#include <stdio.h>
#line 100 / * Встановити лічильник рядків на рядок 100*/
int main(void) { /* рядок 100 */
printf("%d\n",__LINE__); /* рядок 101 */
return 0;
}
#include <stdio.h>
#define mkstr(s) #s
int main(void) {
printf(mkstr(Мені подобається Cі));
return 0;
}
в рядок
#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", xy);
СПИСОК ЛІТЕРАТУРИ
Базова література
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 "ЕСПД. Схемы алгоритмов, программ, данных и систем. Условные
обозначения и правила выполнения".
Додаткова література
15. Войтенко В.В., Морозов А.В. - C, C++. Teopiя та практика, видання 2. – Житомир:
ЖДТУ, 2004. – 324 с.
16. Вінник В.Ю. Алгоритмiчнi мови та основи програмування. Мова C. – Житомир:
ЖДТК, 2007. – 328 с.
17. Керніган Б., Річі Д. Мова програмування C, друге видання. – 232 с.
18. Щедріна О. І. Алгоритмізація та програмування процедур обробки інформації С++.
– Київ: КНЕУ, 2001. – 240 с.