Этот урок курса является факультативным или дополнительным. Для его усвоения необходимо понимать основы двоичной системы счисления, уметь переводить числа между системами счисления, а также знать, что такое битовые или поразрядные операции. Ознакомиться с последними можно в этой лекции.

В языке программирования C доступны следующие поразрядные операции: & (И), | (ИЛИ), ^ (исключающее ИЛИ), << (сдвиг влево), >> (сдвиг вправо) и ~ (поразрядное дополнение до единицы). Давайте рассмотрим их на примерах, но сначала остановимся на том, как выводить числа в других системах счисления на языке C.

В языке C можно присваивать целые числа в десятичной, восьмеричной и шестнадцатеричной системах счисления. Чтобы указать число в восьмеричной системе, перед ним ставится 0, а для шестнадцатеричной — 0x, например:

  int a, b;
  a = 077; // восьмеричное число
  b = 0x1F; // шестнадцатеричное число

Выводить целые числа можно в десятичном, восьмеричном и шестнадцатеричным формате. Вот пример вывода двух переменных в этих форматах:

  printf("%d %o %x %X\n", a, a, a, a);
  printf("%d %o %x %X\n", b, b, b, b);

На экране вы увидите:

63 77 3f 3F
31 37 1f 1F

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

0 000
1 001
2 010
3 011
4 100
5 101
6 110
7 111

Если у нас есть восьмеричное число 037, то в двоичном виде оно будет 011 111, как видно из таблицы.

Задание:
  1. Как будут выглядеть восьмеричные числа 04271 и 03566 в двоичном представлении?
  2. Составьте таблицу перевода шестнадцатеричных цифр в двоичные числа. Переведите числа 7D, FFFF, 2C9 в двоичную систему счисления.

Если бы битовые операции проводились с десятичными числами, то каждый раз понадобилось бы конвертировать их в двоичную форму, что не всегда удобно. В то же время, представление восьмеричных чисел сразу передает двоичный эквивалент, что сокращает необходимость в постоянных пересчетах. Видя число 017, можно представить, как четыре последние бита памяти заполнены единицами.

Давайте перейдем к исследованию поразрядных операций и протестируем их. Вот пример программы:

  int a, b;
  a = 017;
  b = 036;
  printf("0%o & 0%o = 0%o\n", a, b, a & b);
  printf("0%o | 0%o = 0%o\n", a, b, a | b);
  printf("0%o ^ 0%o = 0%o\n", a, b, a ^ b);
  printf("0%o << 2 = 0%o\n", a, a << 2);
  printf("0%o >> 2 = 0%o\n", a, a >> 2);
  printf("~0%o = 0%o\n", a, ~a);

Работа программы даст следующий результат:

017 & 036 = 016
017 | 036 = 037
017 ^ 036 = 021
017 << 2 = 074
017 >> 2 = 03
~017 = 037777777760

Чтобы лучше понять этот результат, взгляните на иллюстрацию ниже:

Результат побитовых операций

Почему получилось такое большое значение в последнем случае? Потому что под целые числа в форматах %d, %o, %X выделяется целых 4 байта памяти.

Задание:
  • Используйте шестнадцатеричные числа для создания аналогичной программы. Поясните её результаты.
  • Экспериментируйте с составлением сложных битовых операций и анализируйте их результаты.

Рассмотрим пример использования битовых операций. Представим, что у нас есть массив и мы хотим определить его "маску", отражающую позиции отрицательных и положительных элементов. Здесь единица (1) будет означать положительный элемент, а ноль (0) — отрицательный. Например, если массив выглядит так {4, -3, 2, 2, 8, -1}, то его "битовая маска" будет 101110, что в восьмеричной системе равно 056. Следующий алгоритм поможет решить эту задачу:

  1. Пусть массив состоит максимум из 32 элементов, и для его "маски" достаточно переменной типа int, назовем её mask, в которую запишем начальное значение 0.
  2. В цикле for обходим элементы массива. При нахождении положительного элемента устанавливаем соответствующий ему бит в mask в 1.
  3. Вывести значение mask в восьмеричном виде на экран.

Как установить определённый бит в 1? Каждый бит в числовом представлении имеет аналог среди степеней двойки. Например:
20 = 0000 0001
21 = 0000 0010
22 = 0000 0100
23 = 0000 1000
24 = 0001 0000
и так далее. Если эта последовательность ещё не усвоена, пересчитайте её. Если применить к переменной mask операцию побитового ИЛИ (|) с соответствующей степенью двойки, то устанавливается один бит в 1. В примере:
(0) 0000 0000 | (25) 0010 0000 = 0010 0000
(32) 0010 0000 | (27) 1000 0000 = 1010 0000

Обратите внимание, что первый элемент массива с индексом 0 должен соответствовать биту, находящемуся в начале двоичной маски. Зная количество элементов массива (N), степень двойки определяется формулой N - i - 1. Например, для третьего положительного элемента в массиве из 10 элементов устанавливается восьмой с конца бит, что значит второй операнд для ИЛИ будет 27.

Как возвести число в степень на языке C? В C возможны операции приведения типов, которые изменяют тип переменной. Стандартная библиотека math.h предлагает функцию pow(), возвращающую число, возведённое в данную степень. Несмотря на её вещественный результат, он может быть преобразован в целый с помощью (int) a. Программа может выглядеть так:

#include <stdio.h>
#include <math.h>
 
#define N 12
 
main () {
 
  int nums[N] = {7, 3, 9, -5, -3, 2, 1, 0, 16, -4, 2, 0};
  int mask = 0, i;
 
  for (i=0; i < N; i++)
    if (nums[i] >= 0)
      mask = mask | (int)pow(2,N-i-1);
 
  printf("%o\n", mask);
}
Задание:

Напишите программу, описанную выше, и исследуйте, как она работает1. Подумайте, как можно вывести на экран двоичное представление восьмеричного числа и попробуйте это реализовать.
1 Если возникают проблемы с компиляцией, добавьте к вызову gcc опцию -lm (например, gcc -o bits bits.c -lm).

Вопросы для самопроверки:

  1. Какие поразрядные операции доступны в языке программирования C?
  2. Как присвоить число в восьмеричной и шестнадцатеричной системах счисления в языке C?
  3. В чем заключается алгоритм определения "битовой маски" для массива, содержащего отрицательные и положительные числа?
  4. Как установить определённый бит в 1, используя битовую операцию ИЛИ?
  5. Как можно возвести число в степень на языке C?

Программа курса:

  1. Описание курса
  2. Введение в язык программирования C
  3. Типы данных в C и форматированный вывод
  4. Символьные типы и управляющие символы в C
  5. Операторы ветвления и switch в C
  6. Циклы и операторы в языке C
  7. Битовые операции в языке C
  8. Посимвольный ввод и вывод в C - буферизация
  9. Переменные, адреса и указатели в C
  10. Передача аргументов по ссылке и значению в C
  11. Форматированный ввод данных с использованием scanf
  12. Генерация псевдослучайных чисел на C
  13. Адресная арифметика в массивах C
  14. Передача массивов в функции и указатели
  15. Строки в языке C - особенности и функции работы
  16. Функции работы со строками в C
  17. Работа со структурами в C - создание и применение
  18. Динамические структуры данных в C
  19. Ввод и вывод данных из файлов в языке C
  20. Передача аргументов в C и работа с файлами
  21. Препроцессор в языке C - директивы и макросы
  22. Создание и компиляция многофайловых программ в C
  23. Использование статических и динамических библиотек в C