
- •1 Основні поняття
- •2 Засоби мови с
- •3 Структура програми, директиви препроцесору
- •4 Елементарні алгоритми
- •5 Бітові операції
- •6 Вкладені алгоритми
- •7 Складні типи даних
- •9 Процедури
- •Int main(void)
- •Void systema(float *yy)
- •Int main(void)
- •Void rekurs(float a, float s, int I)
- •Int main(void)
- •Void reku_for(float z, int I)
- •Int main(void)
- •10 Файли
- •11 Динамічні конструкції даних
- •Int main(void)
- •Int main(void)
- •Int main(void)
- •12 Сортування
- •Void s(int *a, int n)
- •Int main(void)
- •Void s(int *a, int n)
- •Void s(int *a, int n)
- •Void s(int *a, int n)
- •Void s(int *a, int left, int right)
- •Void s(struct pidpr *a, int n)
- •Int main(void)
- •13 Пошук
- •Int p(struct pidpr *a, char *slo, int n)
- •Int p(struct pidpr *a, char *slo, int n)
- •Void s(struct pidpr *a, int n)
- •Int p(char *slo, int n)
- •Int main(void)
- •14 Класи
- •Int main()
- •Void based :: fun()
- •Void poxid :: fun()
- •Int main()
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, 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, вона має такі змінні типу 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, а решта збільшені на одиницю.
Приклад 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);
}
Другий цикл забезпечує видачу на екран масиву букв – аналогів бітів числа n за допомогою функції printf(). Для наочності кожні 4 символи цього масиву розділюються пробілами.
Результати випробування програми різними числами такі:
Введіть ціле число: 1
Число 1 у двійковому представленні = 0000 0001
Введіть ціле число: 4
Число 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, яке випробовується маскою. Для результатів це не має значення, лише були б вони правильними і ми їх одержали. Але зсування маски – поганий тон програмування, вона являє собою константу, яку краще не змінювати.
Запитання для самоперевірки
Поясніть поняття маски.
Як можна за допомогою однієї маски одночасно перевіряти не один, а декілька розрядів числа?
Чи можна в одній комірці пам’яті мати декілька різних масок?
Як виконуються вищеописані порозрядні операції над двома числами різної довжини?
Чи можна відновити значення розрядів числа, які були втрачені в результаті його зсуву?
Чим регламентована довжина маски?
Чому числа -128 і 128, які були введеними під час виконання програми прикладу 5.4, у двійковому представленні виглядають однаково, а саме: 1000 0000? Як компілятор розрізняє числа у подібних випадках?