Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Posibnyk_C_sum.doc
Скачиваний:
9
Добавлен:
29.08.2019
Размер:
1.63 Mб
Скачать

4.3 Цикли

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

Для програмування циклів служать такі спеціальні оператори:

  • for(вираз1; вираз2; вираз3)оператор;

Тут: вираз1 – присвоєння параметру циклу початкового значення;

вираз2 – умова, перевiрка на досягнення кiнцевого значення;

вираз3 – нарощення параметра циклу на заданий крок;

оператор – тiло циклу, один оператор або блок;

  • while(вираз)оператор; Це оператор циклу з передумовою, цикл виконується поки значення виразу не дорівнює нулю;

  • do оператор; while(вираз); Оператор циклу з постумовою, від попереднього відрізняється тим, що спочатку цикл виконується, а потім превіряється умова виходу з циклу, тому незалежно від задовільнення умови він буде виконаний хоча б один раз;

  • continue; Продовження, його дiя полягає в переходi на початок циклу. Тобто, відбувається нарощення параметра i перевiряється умова завершення циклу і, якщо ця умова ще не задовільняється, то цикл продовжує виконуватися спочатку з новим значенням параметра;

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

Слід зауважити, що подібні програмні засоби мають практично всі мови програмування високого рівня. Проте, мова С має свої особливості, які полягають, зокрема, в тому, що вирази оператора циклу можуть мати будь-який вигляд. В заголовку оператора for деякі вирази можуть бути відсутніми, допустима й така конструкція: for(x=0; y<5; z++), тобто вигляд виразів строго не регламентується.

Цикл типу арифметичної прогресії. Цикл з рiвномiрним приростом аргумента є найбiльш вживаним. Число його виконань n можна обчислити за формулою:

де xп, xк, x – вiдповiдно початкове i кiнцеве значення та прирiст

(крок нарощення) параметра циклу.

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

В графічному алгоритмі символ заголовка цього циклу має вигляд двох паралельних ліній, замкнених з боків прямими кутами. Всередині символа пишуть ім’я параметра циклу, знак =, після якого записують початкове і кінцеве значення параметра циклу та його приріст.

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

y=f(z)=3z2-2z+k

при k=3,2 на відрізку [0,2] не змінює свого знака, тобто приймає лише додатні або лише від’ємні значення. Ця функція являє собою параболу, отже змінюється монотонно, тому достатньо дослідити її в декількох точках зміни аргумента. Обчислимо всі її значення на заданому відрізку з кроком 0,2 та представимо результати на екрані у вигляді таблиці.

Графічний алгоритм для даної задачі показаний на рисунку 4.7. Він складається з 6 блоків, з яких перший і останній служать для визначення відповідно початку і кінця його виконання. У другому блоці змінній k присвоюється значення 3,2. Зрозуміло, що перш, ніж виконувати обчислення виразу, необхідно задати значення всіх його змінних. Третій блок містить заголовок циклу, тут сказано, що параметром циклу є змінна z, яка змінюється від 0 до 2 з кроком 0,2. Четвертий і п’ятий блоки складають тіло циклу, де відбувається відповідно обчислення значень функції y=f(z) та вивід результату.

В прикладі 4.3.1 подана програма мовою С яка виконує цей алгоритм. На її початку оголошені змінні z, y, k, всі вони мають тип float, який достатній для нашої задачі. Змінна k ініціалізована під час оголошення константою 3.2. Далі йде заголовок циклу, звернемо увагу на те, що наприкінці його немає крапки з комою, тут цикл лише починається. Тіло циклу взято в фігурні дужки, бо воно являє собою блок, який має два оператори: обчислення значення y і вивід його та аргумента z.

Приклад 4.3.1 – Простий цикл

#include<stdio.h> /* Цикл типу for */

int main(void)

{

float z, y, k=3.2;

for(z=0; z<=2; z+=0.2)

{

y=3*z*z-2*z+k;

printf("y=%5.2f z=%4.1f\n", y, z);

}

getch();

return(0);

}

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

y= 3.20 z = 0.0

y= 2.92 z = 0.2

y= 2.88 z = 0.4

y= 3.08 z = 0.6

y= 3.52 z = 0.8

y= 4.20 z = 1.0

y= 5.12 z = 1.2

y= 6.28 z = 1.4

y= 7.68 z = 1.6

y= 9.32 z = 1.8

Аналізуючи їх, бачимо, що вони нас не задовільняють, бо програма не видала значення y при z=2, хоча в заголовку циклу задана чітка і однозначна умова виконання циклу: z<=2.

Причина цього недоліка полягає в неточному представленні дійсних чисел у пам’яті, через що накопичується похибка обчислень. З метою його усунення в даному випадку можна врахувати похибку та змінити умову, збільшити число 2 на якусь величину, не більшу за один крок, наприклад, на пів-кроку. Тоді умова виконання циклу прийме вигляд: z<=2.1, а до вже показаних результатів додасться ще й такий:

y= 11.20 z = 2.0

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

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

Нижче подано приклад 4.3.2 варіанта даної програми з параметром циклу цілого типу. В її текст внесено дві зміни, а саме: додано змінну n типу int та змінено заголовок циклу. В цьому заголовку початкове значення та нарощення приймають дві змінні – це n i z, але на досягнення кінця циклу перевіряється лише змінна n. Саме вона є параметром, який змінюється від 0 до 10 з кроком 1, тобто приймає 11 значень, оскільки відповідно до наведеної на початку цього розділу формули кількість виконань нашого циклу дорівнює (2-0)/0,2+1=11. Змінена програма видасть усі потрібні результати.

У заголовку циклу програми прикладу 4.3.2 двічі використано операцію , (кома) – послідовне виконання: при набутті початкових значень та при нарощенні змінних n i z. Відомо, що параметр циклу оператора for (в даному випадку це змінна n) компілятор розміщує в регістровій пам’яті (клас register), яка відзначається високою швидкодією. Це вигідно, бо економиться час виконання програми, адже параметр циклу багаторазово змінюється. За рахунок операції послідовного виконання на цей привілей має шанс і змінна z, тобто не відбулося ніякого програшу від того, що вона перестала бути параметром циклу.

Приклад 4.3.2 – Цикл з параметром цілого типу

#include<stdio.h> /* Цикл типу for */

int main(void)

{

float z, y, k=3.2;

int n;

for(n=0, z=0; n<=10; n++, z+=0.2)

{

y=3*z*z-2*z+k;

printf("y=%5.2f z=%4.1f\n", y, z);

}

getch();

return(0);

}

Цикл типу арифметичної прогресії можна побудувати й за допомогою оператора типу while. Нижче подано приклад 4.3.3 програми з його застосуванням для розв’язування поставленої вище задачі.

Приклад 4.3.3 – Застосування оператора while

#include<stdio.h> /* Цикл типу while */

int main(void)

{

float z=0, y, k=3.2;

int n=0;

while(n<=10)

{

y=3*z*z-2*z+k;

printf("num=%2d y=%5.2f z=%4.1f\n", n, y, z);

z+=0.2;

n++;

}

return(0);

}

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

n= 0 y= 3.20 z = 0.0

n= 1 y= 2.92 z = 0.2

n= 2 y= 2.88 z = 0.4

n= 3 y= 3.08 z = 0.6

n= 4 y= 3.52 z = 0.8

n= 5 y= 4.20 z = 1.0

n= 6 y= 5.12 z = 1.2

n= 7 y= 6.28 z = 1.4

n= 8 y= 7.68 z = 1.6

n= 9 y= 9.32 z = 1.8

n=10 y=11.20 z = 2.0

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

У програмі прикладу 4.3.3 параметром циклу є змінна n, яка змінюється від 0 до 10, тому приймає 11 значень. В тілі циклу, обрамленому фігурними дужками, відбувається обчислення зачення y, вивід результатів і нарощення двох змінних: z – на величину 0,2 та n – на 1.

Останній варіант програми можна вдосконалити – скоротити і текст, і час її виконання. Нижче подано фрагмент програми, де ці скорочення відображені.

n=11;z=0;

while(n--)

{

y=3*z*z-2*z+k;

printf("num=%2d y=%5.2f z=%4.1f\n", 10-n, y, z);

z+=0.2;

}

У цьому фрагменті змінна n змінюється не від 0 до 10, як у попередньому прикладі, а навпаки – від 10 до 0. В даному випадку не має значення в якій послідовності змінюється параметр циклу, головне лише, щоб кількість його виконань не змінилася. За рахунок цього вдалося вилучити з тіла циклу його нарощення. Правда, для того, щоб результати виглядали так же, на екран виводиться не значення n, а – 10-n, тобто замість просто імені змінної маємо цілий вираз, тому може статися, що цей виграш буде знівельований.

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

Запитання для самоперевiрки

  1. Пояснiть термiни: цикл, параметр циклу, тiло циклу, кiлькiсть виконань циклу.

  2. Що видаcть фрагмент: m=0; for(k=1;m++;)printf("%d\n",k);?

  3. Що видаcть фрагмент: m=0; for(k=1;m<0;)printf("%d\n",k);?

  4. Що видасть фрагмент: n=1; d=0; for(k=1;n<2;d++)printf("%d",k);?

  5. Що видасть фрагмент: n=0; for(k=1,s=0;n<2;)printf("%d",k);?

  6. Як змiняться результати, якщо в програмi прикладу 4.3.3 опустити фiгурнi дужки, в якi взято тiло циклу?

  7. Якi значення будуть мати змiннi z та i пiсля останнього їх виводу?, пiсля виходу з циклу за межі блока?

  8. Складiть графiчний алгоритм i програму для поставленої в прикладi задачi, використавши оператори if та goto.

  9. Як можна припинити виконання частини операторів тіла циклу і продовжити його виконання з новим нарощеним параметром?

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

  11. Чи можна параметр циклу типу for змінювати за іншим законом, відмінним від арифметичної прогресії?

  12. Чи можна використовувати значення параметра циклу у виразах блока циклу?

  13. Чи можна змінювати значення параметра в тілі циклу помимо його нарощення в заголовку типу for? в тілі циклу типу while?

Ітераційний цикл. Циклiчний iтерацiйний алгоритм характерний тим, що нi кiлькiсть його виконань, нi кінцеве значення параметра циклу заздалегiдь невiдомi. Його суть полягає в тому, що цикл виконується за формулою qi+1 = φ(qi) (i=0, 1, 2, ...), починаючи з деякого наперед заданого числа q0, поки |qi+1-qi| > ε, де ε – задана похибка, а формула φ(q) відома з постановки задачі. Як правило, останнє значення змінної qi є результатом виконання циклу.

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

Рисунок 4.8 – Графічна ілюстрація ходу ітераційного процесу

тут: а – збіжний процес з однієї сторони до межі q*;

б – збіжний процес з обох сторін;

в – розбіжний процес в один бік числової осі;

г – розбіжний процес в обидва боки;

д – зациклення.

Попри привабливість, ітерацiйний циклiчний процес має свої недоліки. Продемонструємо хід ітераційного процесу графічно, він показаний на рисунку 4.8. Аналізуючи його, бачимо, що процес обчислень може бути збіжним (варіанти а, б), розбiжним (варіанти в, г) або зацикленим (варіант д). Звичайно, що користувача програми цікавить лише збіжний процес. Тоді існує певна межа q*, до якої він наближається, тобто можна сказати, що

Вважають, що, якщо вiдстань |qi+1 - qi| стане меншою за деяку, наперед задану малу величину (похибку ε), то вона також менша за похибку обчислення, тобто |qi+1 - q*| < ε, тодi iтерацiйний процес припиняється.

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

Дослідимо циклічний ітераційний процес та встановимо умову його збіжності на прикладі розв’язування алгебраїчного нелінійного рівняння методом простої ітерації. Нехай задано рівняння f(x)=0. Для забезпечення збіжності помножимо це рівняння на постійний коефіцієнт k, тобто представимо його у вигляді k f(x)=0. Перетворимо це рівняння до вигляду, вигідного для ітерації, якимось із способів, наприклад, додамо в його ліву і праву частину число x, з курсу математики відомо, що подібні маніпуляції не змінюють кореня рівняння. Тоді одержимо ітераційну формулу:

x=x+k f(x). (4.1)

Замінимо її на систему функцій:

та прослідкуємо їхню поведінку в ході ітераційного процесу. Функція y=φ(x) може мати один із чотирьох можливих варіантів вигляду, показаних на рисунку 4.9, а функція y=x являє собою пряму лінію, яка проходить під кутом 450 через початок координат.

Аналізуючи рисунок 4.9, знаходимо, що ітераційний процес буде збіжним у випадках а і б, де кут нахилу дотичної (уявної, на рисунку вона відсутня) до кривої y=φ(x) менший за |450| на всьому інтервалі зміни x, причому швидкість збігання тим більша, чим цей кут менший, тобто ближчий до 0, а найкраще, якщо він дорівнюватиме нулю. Для того, щоб цю умову можна було використати в математичних виразах, замінимо її на тотожну іншу: тангенс кута нахилу вищевказаної дотичної повинен бути меншим за одиницю або перша похідна функції y=φ(x) – менша за одиницю, тобто

|φ'(x)|<1. (4.3)

Оскільки найкраща збіжність забезпечується при рівності нулю кута нахилу дотичної до кривої y=φ(x), в якості умови збіжності приймемо рівність нулю її першої похідної, тобто φ'(x)=0 (в цьому випадку модульні дужки стають зайвими).

Підставимо в цю умову збіжності значення φ(x), відоме з виразу 4.2, тоді φ'(x)=[x+k f(x)]'=1+k f'(x)=0. Звідси k = -1/f'(x), підставивши його в формулу 4.1, одержимо: x=x-f(x)/f'(x) або

Це відома формула Ньютона, її математичне обгрунтування можна знайти в підручниках з обчислювальної математики. Вона забезпечує збіжність ітерації на певному відрізку [a, b], названому інтервалом ізоляції кореня, де задовільняються вимоги: sign(f′(x))=const та sign(f(a)) ≠ sign(f(b)), тобто зберігається незмінність знака першої похідної функції f(x) (вона монотонно або зростає, або спадає), а також на кінцях відрізка ця функція має різні знаки. Тоді x0 вибирають з діапазону [a,b]. Цю формулу можна поширити й на інші подібні процеси.

Покажемо цикл ітераційного типу на прикладі розв’язування рівняння

f(z)=z3-z2+kz-c=0,

де z – корінь рівняння, k=3,2, c=1,4.

Це відоме в нафтогазовій справі рівняння Редліха-Квонга [8], яке застосовують для обчислення коефіцієнта надстислості газу z при різних значеннях температури і тиску. З його фізичного змісту відомо, що значення z може знаходитися в межах [a, b], де a=0, b=2.

Спробуємо його розв’язати методом простої ітерації. Для цього перетворимо рівняння до вигляду вигідного для ітерації, додамо до лівої і правої його частин z. Зауважимо, що від цього його корінь не зміниться. Тоді воно прийме такий вигляд: z=z3-z2+(k+1)z-c. За початкове наближення кореня візьмемо значення 1 – середину відрізка [0,2]. Тоді z0=1, а ітераційна формула матиме такий вигляд:

zi+1=zi3-zi2+(k+1)zi-c.

Нам немає необхідності зберігати в пам’яті всі значення z, лише два сусідні наближення: zi+1 та zi, які потрібні для перевірки умови завершення циклу так, як про це сказано на початку цього розділу. Позначимо їх відповідно p=zi+1, а z=zi, похибку ε візьмемо рівною 10-5. Тоді ітераційну формулу одержимо в такому вигляді: p=z3-z2+(k+1)z-c при початковому наближенні z=1, а умову виконання (продовження) циклу – в такому: |z-p|>ε при ε=10-5.

Графічний алгоритм для цієї задачі показаний на рисунку 4.10. В його 2-му блоці початкове значення присвоєно змінній p=1, а не z, як сказано вище, це зроблено для того, щоб цикл почав працювати. В блоці 3 це та наступні значення p будуть переприсвоєні змінній z, а далі – використовуватися для обчислення за ітераційною формулою.

Нижче (приклад 4.3.4) показано текст програми, яка реалізовує цей алгоритм за допомогою оператора циклу типу do–while. Її ідентифікатор eps означає ε, а решта мають таку ж назву, як і в графічному алгоритмі. Операцію взяття за модулем у виразі оператора while реалізовано за допомогою функції fabs(), яка призначена для обробки чисел типу float. (Зауважимо, що інший її варіант abs() використовується для типу int). Наприкінці програми передбачено вивід результату p. Зауважимо, що замість нього можна було б вивести й z, обидві ці змінні відрізняються між собою не більше, ніж на похибку eps, але значення p – останнє наближення, тому воно точніше.

Приклад 4.3.4 – Циклічна програма ітераційного типу

#include<stdio.h> /* Рівняння Редліха–Квонга */

#include<math.h> /* Файл math.h приєднаний для функції fabs() */

int main(void)

{

float z, p=1, k=3.2, c=1.4, eps=1e-5;

do

{

z=p;

p=z*z*z-z*z+(k+1)*z-c;

}

while(fabs(z-p)>eps);

printf("Korin= %f \n", p);

getch();

return(0);

}

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

Floating point error: Overflow.

Воно означає, що відбулося переповнення пам’яті. Спробуємо знайти причини неполадків і усунути їх. Спочатку перевіримо чи задовільняються вищеперечислені вимоги до інтервалу ізоляції кореня [0,2]. Нагадаємо, що першою з них була незмінність на ньому знака першої похідної функції f(z), яка матиме такий вигляд:

f′(z)=3z2-2z+k.

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

Друга умова вимагає різних знаків функції f(z)=z3-z2+kz-c на кінцях відрізка [0,2]. Виконавши необхідні обчислення, знайдемо, що f(0)=-1,4 – від’ємне число, а f(2)=8-4+3,2*2-1,4=9 – додатнє, тобто, що й ця умова теж задовільняється.

Тоді залишається перевірити чи задовільняється умова збіжності (4.3). Аналізуючи результати, видані програмою прикладу 4.3.3, бачимо, що всі вони більші за одиницю, отже, причина неполадків полягає в тому, що ця умова не задовільняється. В цьому можна також переконатися шляхом виводу проміжних результатів ітерації, для цього функцію їх виводу слід вставити в тіло циклу програми прикладу 4.3.4.

Забезпечимо збіжність, застосуємо вищенаведену формулу Ньютона (4.4), що й зроблено в програмі прикладу 4.3.5.

Приклад 4.3.5 – Забезпечення збіжності, застосування формули

Ньютона

#include<stdio.h> /* Метод Ньютона */

#include<math.h>

int main(void)

{

float z, p=1, k=3.2, c=1.4, eps=1e-5;

do

{

z=p;

p=z - (z*z*z-z*z+k*z-c)/(3*z*z-2*z+k);

}

while(fabs(z-p)>eps);

printf("Корінь рівняння= %f \n", p);

getch();

return(0);

}

Вона видасть такий результат:

Корінь рівняння= 0.474471

Підставивши це значення кореня в наше рівняння, переконуємося в тому, що воно вірне, f(0,474471)=-0,0000001, тобто одержане значення дорівнює нулю з незначною похибкою.

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

Ітераційний цикл можна побудувати й за допомогою оператора типу for, нижче показано цей варіант програми у прикладі 4.3.6.

Приклад 4.3.6 – Ітераційний цикл з оператором for

#include<stdio.h> /* Ітераційний цикл з оператором for */

#include<math.h>

int main(void)

{

float z, p, k=3.2, c=1.4, eps=1e-5;

for(z=1, p=10; fabs(z-p)>eps; p=z-(z*z*z-z*z+k*z-c)/(3*z*z-2*z+k))z=p;

printf("Корінь рівняння= %f \n", p);

getch();

return(0);

}

Вона видасть той же вірний результат. У списку заголовка оператора for змінній p присвоєно число 10 – формальне значення (будь-яке), яке перевищує змінну z на число, більше за похибку. Це зроблено для забезпечення виконання циклу за першим разом (для початку ітераційного процесу).

Запитання для самоперевiрки

  1. Який цикл названо тут ітераційним і чому? Поясніть його суть.

  2. Поясніть поняття: збіжний, розбіжний, зациклений процеси.

  3. Поясніть умову забезпечення збіжності ітераційного процесу.

  4. Чи можна розбіжний ітераційний процес зробити збіжним?

  5. Назвiть оператори циклу з передумовою та постумово. Чим вони вiдрiзняються мiж собою?

  6. Як можна використати оператор типу for для складання програми ітераційного типу?

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

  8. У програмах прикладів 4.3.5 та 4.3.6 (з циклом типу for) забезпечте видачу на екрані загальної кількості ітерацій, затрачених на обчислення кореня.

  9. Складiть програму для розв’язування рівняння, використавши оператор циклу типу while.

  10. Чому в програмах, як результат, передбачено вивід значення змiнної p? Чи не краще виводити z? Яка різниця між цими змінними?

  11. Як буде виконуватись оператор do-while прикладу 4.3.5, якщо в його дужках замiсть виразу fabs(z-p)>e буде стояти число 1?, інше число?, якась буква? Як у цих випадках забезпечити видачу вірного результату?

  12. Алгоритми якого типу реалізовані вищенаведеними програмами? Чи має тип арифметичної прогресії цикл з оператором for?

  13. Які типи алгоритмів, крім циклічних, маємо в програмах вищенаведених прикладів?

  14. Складiть графiчний алгоритм i програму для уточнення квадратного кореня x з числа a за формулою: x=(x+a/x)/2.

  15. Знайдiть iтерацiйну формулу для добування кореня будь-якого степеня з довiльного числа a.

5 БІТОВІ ОПЕРАЦІЇ

У техніці зустрічаються задачі, в яких для ідентифікації стану об’єкта достатньо лише одного або декількох біт. Це, наприклад, електрична напруга на світловому табло бурового майстра, яка може бути ввімкненою або вимкненою (1 біт – 0 або 1), 5 станів бурової установки (3 біти – 5 комбінацій цифр 0 і 1), системи автоматизованого управління технологічними об’єктами низового рівня, комп’ютерні мережі, системи передачі та обробки сигналів, побутові спеціалізовані мікро-ЕОМ та ін. У подібних випадках для запам’ятовування інформації часто застосовують одну декількабітову комірку пам’яті, кожний біт якої має власне призначення.

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

Нехай, наприклад, маємо однобайтову комірку пам’яті, з яких потрібна нам інформація зберігається в другому розряді (зправа). Решта 7 розрядів комірки можуть використоватися для інших цілей. Тоді маска повинна бути не менше, ніж 2-розрядною. Візьмемо для неї теж однобайтову комірку пам’яті, куди запишемо вісімкове число 02 (двійкове 0000 0010), оскільки за її допомогою оброблятимемо 2-й розряд. Зауважимо, що немає особливої необхідності представляти число 2 у вісімковій системі числення, однак, під час обробки двійкових чисел програмісти часто користуються їх вісімковим еквівалентом. Нагадаємо, що двійкові числа вигідно групувати вісімковими тріадами, тому з метою підкреслення цього факта вдається підвищити “читабельність” програми.

Покажемо застосування запропонованої вище маски на декількох прикладах. У першому ввімкнемо другий двійковий розряд (присвоїмо значення 1, якщо він дорівнює 0, і залишимо 1 у протилежному випадку). Для випробування візьмемо два числа: 17 і 18, назвемо їх відповідно i1 та i2. Після запису в однобайтову комірку типу unsigned char вони матимуть вигляд відповідно: 0001 0001 і 0001 0010. Ці два числа спеціально підібрані так, щоб показати, що, по-перше, у 2-му розряді вони мають різні значення (перше – нуль, а друге – одиницю), а по-друге, що одиниці в інших розрядах (у нашому випадку 5-й і 1-й) ні не впливають на результати, ні не змінюються під час маніпуляції бітами. Це робить програма, показана в прикладі 5.1.

Приклад 5.1 – Застосування маски

#include<stdio.h> /* Ввімкнення 2-го розряду */

int main(void)

{unsigned char mask=02, i1=17, i2=18;

i1 |= mask; i2 |= mask;

printf("i1=%d i2=%d\n", i1, i2);

getch();

return(0);

}

У цій програмі оголошено такі 3 змінні цілого беззнакового типу довжиною 8 біт (unsigned char):

  • mask – маска, mask = 02 (двійкове 0000 0010);

  • i1 – для імітації вимкненого 2-го розряду (i1 = 17 = 0001 0001);

  • i2 – для імітації ввімкненого 2-го розряду (i2 = 18 = 0001 0010).

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

i1=19 i2=18

При виконанні програми заданий розряд (в нашому прикладі другий) результату повинен дорівнювати одиниці в будь-якому випадку за рахунок застосування операції порозрядного включного “АБО”, тобто відбувається ввімкнення другого розряду чисел i1 або i2, якщо він був вимкнений. Покажемо це на такій схемі:

маска mask: 0000 0010 0000 0010

числа: 0001 0001 0001 0010

результат операції |: 0001 0011 0001 0010

Аналізуючи результати, бачимо, що в обох числах: i1 та i2 другий розряд став дорівнювати одиниці, а решта залишилися незмінними.

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

i1 &= ~mask; i2 &= ~mask;

Тут використано дві операції: інвертування (зворотній код) маски і її порозрядне “I” з цими числами. Одержимо такі результати:

i1=17 i2=16

Нижче подана схема такої маніпуляції розрядами.

маска mask: 0000 0010 0000 0010

результат інвертування маски: 1111 1101 1111 1101

числа: 0001 0001 0001 0010

результат операції &: 0001 0001 0001 0000

Виконання третього прикладу спричинить перемикання 2-го розряду числа з одиниці на нуль та навпаки за допомогою вставки в програму прикладу 5.1 таких операторів:

i1 ^= mask; i2 ^= mask;

Тут використовується операція порозрядного виключного “АБО”. В результаті її виконання одержимо такі значення:

i1=19 i2=16

Хід виконання операторів показано нижче на схемі:

маска mask: 0000 0010 0000 0010

числа: 0001 0001 0001 0010

результат операції ^: 0001 0011 0001 0000

Аналізуючи цю схему, бачимо, що число i1 не мало одиниці в другому розряді (дорінювало 17), а виконання операції ^ дало результат 19, тобто другий розряд змінився на протилежний. З другим числом i2 сталося те саме – результат став дорівнювати 16.

В черговому прикладі перевіримо на наявність одиниці 2-й розряд чисел i1 та i2, тобто одержаний результат повинен дорівнювати 1 (Так), якщо число має одиницю в 2-му розряді, і 0 (Ні) – в протилежному випадку. Для цього застосуємо операцію порозрядного “І” маски з цими числами. Її результат дорівнює mask лише тоді, коли якесь із них матиме одиницю в тих же розрядах, що й число mask. Таку одиницю має число i2. Тоді програма прикладу 5.1 матиме такий оператор:

printf("rez1=%d rez2=%d\n", (mask & i1)==mask, (mask & i2)==mask);

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

rez1=0 rez2=1

Нижче подана схема обчислень, яка його пояснює:

маска mask: 0000 0010 0000 0010

числа: 0001 0001 0001 0010

результати операції &: 0000 0000 0000 0010

результати операції ==: 0 (Ні) 1 (Так)

Покажемо ще так звану змінну циклічну або пересувну маску. Застосуємо її для випробування змінної i1 на наявність розрядів зі значенням 1 та видача їх порядкових номерів. Це показано в програмі прикладу 5.2.

Приклад 5.2 – Змінна маска

#include<stdio.h> /* Пересувна маска */

int main(void)

{ int num;

unsigned char mask=02, i1=17;

for(num=01, mask=01; num<=010; num++, mask<<=1)

if((mask & i1)==mask)printf("num=%d ", num);

getch();

return(0);

}

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

num=1 num=5

Нижче показано хід виконання операцій:

число i1: 0001 0001 0001 0001

маска mask: 0000 0001 0001 0000

результат операції &: 0000 0001 0001 0000

результ. операції ==: 1 1

значення num: 1 5

Оскільки число і1 мало одиниці в 1-му і 5-му розрядах, значення змінної num було виведено два рази: зі значеннями 1 і 5.

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

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

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

Програма, яка демонструє таку можливість, подана в прикладі 5.3. На її початку оголошені дві змінні: i1=17 та i2=18, відомі з попереднього прикладу, допоміжна змінна dop та змінна c типу short. Змінна c призначена для зберігання одночасно двох чисел: i1 та i2.

Приклад 5.3 – Зберігання двох чисел в одній комірці пам’яті

#include<stdio.h> /* Два числа в одній комірці пам’яті */

int main(void)

{

unsigned char i1=17, i2=18, dop;

short c;

c=i1; c<<=8; c+=i2; dop=c;

printf("i1&i2=%p i1=%d i2=%d\n", c,c>>8,dop);

return(0);

}

На початку програми значення змінної с=i1, потім воно посувається вліво на 8 розрядів (c<<=8;) – робиться місце для другого числа i1. В результаті виконання операції присвоєння: c+=i2; змінна c містить обидва числа. Функція printf() виводить 3 числа: c, i1 та i2. Результати побачимо в такому вигляді:

i1&i2=1112 i1=17 i2=18

Зауважимо, що число 1112 виведене в 16-й системі числення за допомогою специфікатора p. Якщо представимо кожний його розряд двійковими тетрадами, то одержимо таке двійкове число:

0001 0001 0001 0010

Можна помітити, що його лівий байт містить число 17, а правий –18 у двійковій системі числення.

Для виділення числа 18 у програмі використовується допоміжна змінна dop, вона приймає це значення шляхом присвоєння dop=c; Оскільки змінна dop має довжину 1 байт, то вона приймає лише останні 8 розрядів числа c, тому дорівнює 18. Для зчитування числа 17 зі змінної c достатньо її зсунути на 8 розрядів управо, тоді число 18 пропадає, а 17 залишається.

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

Знакорозмірна форма числа. Представлення числа в пам’яті залежить від технічної реалізації. Деякі комп’ютери для знака призначають один старший (крайній лівий) розряд комірки пам’яті, який називається знаковим. Якщо число додатнє, то в цьому розряді знаходитиметься число нуль, інакше – одиниця. Тоді решта розрядів використовуються для запису модуля числа. Нехай, наприклад, маємо комірку пам’яті типу signed char (8 біт), тоді в неї можна буде записати цілі числа від -127 до +127 (від 1111 1111 до 0111 1111 у двійковій системі числення). Така форма представлення числа називається знакорозмірною.

Одним із недоліків цього способу є те, що число нуль може бути як додатнім, так і від’ємним, тобто +0 або -0, бо в двійковому представленні вони дорівнюють відповідно 0000 0000 або 1000 0000. Це може спричинити плутанину, бо кожного разу приходиться враховувати, що збережене в пам’яті число 0 насправді може й не бути нулем, оскільки в старшому розряді може містити одиницю.

Метод доповнення до двох. З метою позбавитися від цього недоліка часто застосовується прийом, який називається методом доповнення до двох. Його суть полягає в тому, що додатнє число зберігається в звичайному двійковому вигляді, а від’ємне – зміненим. Покажемо це на прикладі однобайтової комірки, відомо, що в неї можна записати додатнє беззнакове число (типу unsigned char) з діапазону від 0 до 255 (двійкові від 0000 0000 до 1111 1111). Якщо число знакове, тобто може бути як додатнім, так від’ємним (signed char), то діапазон його значень становитиме від -128 до 127, причому в найстаршому розряді від’ємного числа матимемо ту ж таки одиницю, що і в знакорозмірному представленні, а додатнього – нуль.

Під час запису в пам’ять таких обчислювальних машин додатнє число записується у звичному для нього вигляді, а від’ємне –автоматично інвертується і додається до нього одиниця. Нехай, наприклад, маємо число 4, тоді в комірці пам’яті типу signed char воно виглядатиме як 0000 0100. Ознакою того, що число додатнє, є нуль у його старшому (крайньому лівому) розряді. Якщо число дорівнює -4, то інвертоване воно виглядатиме, як 1111 1011, а після додавання одиниці – 1111 1100. При зчитуванні стає відомо, що воно від’ємне, бо має одиницю в старшому розряді, тоді для визначення модуля його віднімають від 9-розрядного (для типу char) числа 256. Нижче це показано на прикладі числа -4.

Число 4: 0000 0100

інвертоване число 4: 1111 1011

число -4: 1111 1100

число 256: 1 0000 0000

результат віднімання (модуль числа -4): 0000 0100

Якщо число дорівнює 0, то воно не може бути від’ємним, бо в пам’яті матиме вигляд: 0000 0000 – у старшому розряді має нуль. А двійкове від’ємне (старший розряд дорівнює одиниці) число 1000 0000 – не нуль, воно дорівнює -128, покажемо це:

Число 256: 1 0000 0000

число -128: 1000 0000

результат віднімання (модуль числа 128): 1000 0000

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

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

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

Приклад 5.4 – Читання бітів цілого числа

#include<stdio.h> /* Візуальне представлення двійкового коду числа */

int main(void)

{char n, i, m=8*sizeof(char);

char b[8*sizeof(char)+1];

printf("\n Введіть ціле число: "); scanf("%d",&n);

printf("Число %d у двійковому представленні = ",n);

for(i=m-1; i>=0; i--, n>>=1)b[i]=(01 & n)+48;

b[m]='\0';

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

{

printf("%c",b[i]);

if(!((i+1) % 4))printf(" ");

}

return(0);

}

Читання бітів цілого числа виконує програма прикладу 5.4, вона має такі змінні типу char (нагадаємо, що тип char – цілий):

  • n – число, яке й потрібно показати в бітах;

  • i – лічильник, параметр циклу;

  • m – кількість бітів числа, дорівнює 8 при довжині типу char в один байт;

  • b[ ] – літерний масив, довжина якого в байтах дорівнює кількості двійкових розрядів числа n плюс 1 для літери '\0' наприкінці масиву.

Основу програмного блоку складають два послідовні цикли, обидва типу for, з яких перший призначений для одержання масиву букв 0 або 1 (не слід тут плутати поняття: буква 1 і число 1 біт). Його параметр i змінюється від m-1 до 0 з кроком -1 (m разів), такі значення (від m до 0) забезпечать запис у масив b[ ] букв 0 та 1 в порядку їх розташування в комірці пам’яті – зліва направо. Паралельно в циклі число n зсувається вправо на 1 біт (операція n>>=1), тоді кожний крайній правий біт порівнюється (операція &) з маскою-константою 01. В результаті одержуємо число 1 або 0, яке перетворюється в символ шляхом додавання до нього числа 48. Нагадаємо, що ASCII-код букви 0 дорівнює 48, а решта збільшені на одиницю.

Внутрішній цикл забезпечує видачу на екран масиву букв – аналогів бітів числа n за допомогою функції printf(). Для наочності кожні 4 символи цього масиву розділюються пробілами.

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

Введіть ціле число: 1

Число 1 у двійковому представленні = 0000 0001

Введіть ціле число:

Число 4 у двійковому представленні = 0000 0100

Введіть ціле число: -4

Число -4 у двійковому представленні = 1111 1100

Введіть ціле число: 0

Число 0 у двійковому представленні = 0000 0000

Введіть ціле число: 127

Число 127 у двійковому представленні = 0111 1111

Введіть ціле число: 128

Число -128 у двійковому представленні = 1000 0000

Введіть ціле число: -128

Число -128 у двійковому представленні = 1000 0000

Введіть ціле число: -1

Число -1 у двійковому представленні = 1111 1111

Аналізуючи їх, можна зробити такі висновки:

  • число n приймає значення з діапазону від -128 до +127;

  • для зберігання числа n застосовується метод доповнення до двох;

  • для цілих чисел (у даному випадку типу char) не існує такого поняття, як переповнення пам’яті.

Порівнюючи приклад 5.2 пересувної маски з цією програмою, знаходимо різницю між ними в тому, що в першому випадку для читання бітів відбувався зсув маски, а в другому – навпаки, зсувається число n, яке випробовується маскою. Для результатів це не має значення, були б вони лише вірними і ми їх одержали. Але зсування маски – поганий тон програмування, вона являє собою константу, яку краще не змінювати.

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

  1. Поясніть поняття маски.

  2. Перечисліть види масок за кількістю розрядів, за типом числа.

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

  4. Поясніть виконання операцій зсуву розрядів уліво та вправо.

  5. Приведіть альтернативні способи (без маски) розв’язування задач прикладу 5.1.

  6. Чи можна в одній комірці пам’яті зберігати декілька різних чисел типу int або float?

  7. Як можна за допомогою однієї маски одночасно перевіряти не один, а декілька розрядів числа?

  8. Чи можна в одній комірці пам’яті мати декілька різних масок?

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

  10. Чи можна відновити значення розрядів числа, які були втрачені в результаті його зсуву?

  11. Як зв’язані між собою № розряду, з яким працює маска, і кількість її розрядів?

  12. Чим регламентована довжина маски?

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

  14. Для чого в програмі прикладу 5.2 використовується допоміжна змінна unsigned char dop, чи можна без неї обійтися?

  15. Чому числа -128 і 128, які були введеними під час виконання програми прикладу 5.4, у двійковому представленні виглядають однаково, а саме: 1000 0000? Як компілятор розрізняє числа у подібних випадках?

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]