Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Рацеев С.М. Программирование на языке Си.pdf
Скачиваний:
366
Добавлен:
23.03.2016
Размер:
1.65 Mб
Скачать

12. РАБОТА С БИТАМИ ПАМЯТИ

12.1. Битовые операции

Прежде чем рассматривать битовые операции, уточним, каким именно образом целые числа представляются в двоичном виде. Что касается беззнаковых целых чисел, то здесь все просто. Например, 8-разрядные беззнаковые целые числа представляются следующим образом:

Десятичное число

8-разрядное двоичное представление числа

0

00000000

1

00000001

2

00000010

3

00000011

255

11111111

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

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

00000001.

Во-вторых, инвертировать все биты данного числа: 11111110.

В-третьих, прибавить к полученному числу единицу: 11111111.

Это и есть 8-разрядный дополнительный код числа -1. Если теперь сложить дополнительный код числа -1 и двоичное представление числа 1, то получим такое число:

100000000.

196

Поскольку полученное число тоже рассматривается как 8-разрядное, то старший бит 1 не учитывается, и тем самым 8-разрядный результат суммирования равен 0.

Старший бит дополнительного кода двоичных чисел отвечает за знак числа: старший бит неотрицательных чисел равен 0, отрицательных – 1. Ниже приведена таблица 8-разрядных знаковых двоичных чисел.

Число

Дополнительный код

Число

Дополнительный код

0

00000000

-128

10000000

1

00000001

-127

10000001

2

00000010

-126

10000010

126

01111110

-2

11111110

127

01111111

-1

11111111

Битовые операции рассматривают операнды как упорядоченные наборы битов, каждый бит может иметь одно из двух значений – 0 или 1. Такие операции позволяют программисту манипулировать значениями отдельных битов.

Операции

Назначение

&

поразрядное И

|

поразрядное ИЛИ

^

поразрядное исключающее ИЛИ

<<

сдвиг влево

>>

сдвиг вправо

~

поразрядное НЕ (дополнение до единицы)

Операции сдвига << и >> сдвигают биты первого операнда влево или вправо на количество битов, заданных вторым операндом. При этом биты, уходящие за пределы разрядной сетки, теряются, а освободившиеся двоичные разряды заполняются нулями. Например, для 8-разрядного беззнакового числа (unsigned char) 27

имеют место следующие равенства:

 

 

 

00011011 << 1 = 00110110

(27

<< 1

= 54),

00011011 << 2 = 01101100

(27

<< 2

= 108),

197

00011011 << 3 = 11011000

(27

<< 3 = 216),

00011011 << 4 = 10110000

(27

<< 4

= 176),

00011011 << 5 = 01100000

(27

<< 5

= 96),

00011011 >> 1 = 00001101

(27

>> 1

= 13),

00011011 >> 2 = 00000110

(27

>> 2

= 6).

Если операция сдвига вправо (>>) применяется к переменным знакового типа (signed), то освободившиеся разряды заполняются единицами. Например, для 8-разрядного знакового числа (char) -27

имеют место быть такие равенства:

 

 

 

11100101 << 1 = 11001010

(-27 << 1

= -54),

11100101 << 2 = 10010100

(-27

<< 2

= -108),

11100101 << 3 = 00101000

(-27

<< 3

= 40),

11100101 >> 1 = 11110010

(-27

>> 1

= -14),

11100101 >> 2 = 11111001

(-27

>> 2

= -7).

Заметим, что сдвиг влево (вправо) соответствует умножению (делению) левого операнда на степень числа 2, равную значению второго операнда.

Операция ~ является одноместной, она изменяет каждый бит целого числа на обратный. Операции &, | и ^ – двуместные; операнды этих операций – целые величины одинаковой длины. Операции выполняются попарно над всеми двоичными разрядами операндов.

Определение битовых операций:

Бит левого

Бит правого

~x

x & y

x | y

x ^ y

операнда (x)

операнда (y)

 

 

 

 

0

0

1

0

0

0

0

1

1

0

1

1

1

0

0

0

1

1

1

1

0

1

1

0

Например:

Операция ~:

Операция &:

Операция |:

Операция ^:

~01010101

01010101 &

01010101 |

01010101 ^

 

00000001

00101011

01011010

10101010

00000001

01111111

00001111

198

Приведем также некоторые формулы с использованием операций &, | и ^, которые нам понадобятся в дальнейшем:

x | 1 = 1, x | 0 = x,

x & 1 = x, x & 0 = 0, x ^ 1 = ~x, x ^ 0 = x.

12.2. Примеры с использованием битовых операций

Пример 1. Написать функцию, которая печатает двоичное представление целого числа a.

Функция DisplayBits получает в качестве параметра число a, которое требуется представить в двоичном виде, и количество занимаемых данным числом бит равно n.

#include <stdio.h>

void DisplayBits(int a, int n)

{

int i, bit;

for (i = n - 1; i >= 0; i--)

{

/* выделяем i-й справа бит */ bit = (a >> i) & 1;

printf("%d ", bit);

}

}

int main( )

{

int a; scanf("%d", &a);

DisplayBits(a, sizeof(int)*8); return 0;

}

199

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

#include <stdio.h>

void Print(unsigned int a)

{

if (a)

{

Print(a >> 1); printf("%u ", a&1);

}

}

int main( )

{

unsigned int a; scanf("%u", &a); Print(a);

return 0;

}

Пример 2. Пусть имеется целое число a. Требуется i-й справа бит в числе a установить в значение 1, не меняя значения других битов.

Обозначим через b целое число, содержащее 1 в i-м бите и 0 – в остальных. Его можно получить так: b = 1 << i. Тогда требуемое число получается таким образом: a | b.

a |= 1 << i;

Пример 3. Обобщим предыдущую задачу. Пусть имеется целое число a. Требуется k подряд идущих бит, начиная с i-й справа позиции, установить в значение 1, не меняя значения других битов.

Рассмотрим число b=(0…01…1)2, заданное в двоичном виде, в котором сначала следуют только нулевые биты, а затем следуют

ровно k единичных бит. Учитывая формулу

1 + 2 + 22 + 2k-1 = 2k – 1,

200

которая верна для любого натурального k, число b равно значению 2k – 1, что на языке битовых операций означает такое представление:

b = (1 << k) – 1.

Для решения данной задачи достаточно сдвинуть все биты в числе b на i позиций влево и применить операцию побитового ИЛИ к полученному числу и числу a:

a | ((1 << k – 1) << i).

В итоге, получаем такое выражение: a |= (1 << k – 1) << i.

Пример 4. Пусть имеется целое число a. Требуется i-й справа бит в числе a установить в значение 0, не меняя значения других битов.

Рассмотрим целое число b, содержащее 0 в i-м бите и 1 – в остальных: b = ~(1 << i). Тогда требуемое число получается так: a & b.

a &= ~(1 << i);

Пример 5. В целом числе a требуется инвертировать i-й справа бит, не меняя значения других битов.

a ^= (1 << i);

Пример 6. Даны две переменные x и y целого типа. Требуется i-й справа бит переменной x поставить на j-ю справа позицию переменной y, не изменяя при этом остальные биты в y.

Сначала обнулим все биты в переменной x, кроме i-го справа бита xi, который поставим на j-ю справа позицию в x:

((x >> i) & 1) << j.

Затем обнулим j-й справа бит в переменной y, не изменяя значения остальных битов:

y & ~(1 << j).

Для решения задачи осталось применить побитовую операцию ИЛИ к предыдущим двум выражениям:

y = ((x >> i) & 1) << j | y & ~(1 << j).

201

Пример 7. Найти количество единичных битов в целом неотрицательном числе a.

Функция BitCount каждый раз проверяет состояние младшего бита, постепенно сдвигая все биты на одну позицию вправо, и так до тех пор, пока не будут проверены все n битов целого числа.

short BitCount(unsigned int a, short n)

{

short count; int i;

count = 0;

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

{

if (a & 1) count++;

a >>= 1;

}

return count;

}

Функцию BitCount можно немного оптимизировать, учитывая то обстоятельство, что если после очередного сдвига всех битов на одну позицию вправо число a становится нулевым, то в нем нет более единичных битов. Поэтому нет необходимости всегда просматривать все n битов, а лишь до тех пор, пока не будет учтен самый старший (левый) единичный бит.

short BitCount2(unsigned int a)

{

short count; count = 0; while (a != 0)

{

if (a & 1) count++;

a >>= 1;

202

}

return count;

}

Последнюю функцию можно компактно записать с помощью рекурсивной функции:

int Count (unsigned int a)

{

return a ? (a & 1) + Count(a >> 1) : 0;

}

Пример 8. Дано целое число n > 0, являющееся некоторой степенью числа 2: n = 2k. Найти целое число k – показатель этой степени.

int Degree(unsigned int n)

{

int k; k = -1;

while (n)

{

n >>= 1; k++;

}

return k;

}

Пример 9. Пусть имеется некоторое натуральное число n. Проверить, является ли оно степенью числа 2, то есть найдется ли такое целое неотрицательное число k, что n = 2k.

int Deg_of_2(unsigned int n)

{

return (n & (n-1)) == 0;

}

203

Пример 10 (алгоритм быстрого возведения в степень).

Пусть требуется вычислить an, где a и n – некоторые целые числа, причем n ≥ 0. Рассмотрим такие степени числа a:

a, a2 , a4 , a8 , ..., a2t ,

где t = [log2 n]. Заметим, что каждое число из указанной последовательности получается путем умножения предыдущего числа самого на себя. Представим число n в виде такого разложения:

n = nt 2t +nt 1 2t 1 +...+n1 2 +n0 ,

где n0, n1, …, nt – числа из множества {0, 1}. Тогда число an может быть вычислено таким образом:

an = an0 an1 2an2 4... ant1 2t1 ant 2t .

Поэтому количество операций умножения при вычислении an по данному методу не превосходит 2 log2 n.

long Degree(long a, unsigned long n)

{

long deg,

/* степени числа a: a, a2, a4, a8,… */

rez;

/* результат возведения в степень */

rez = 1;

 

deg = a;

 

while (n != 0)

{

if (n & 1) rez *= deg;

deg *= deg; n >>= 1;

}

return rez;

}

Если записать данную функцию с помощью рекурсии, то получится такая компактная функция:

long Deg(long a, unsigned long n)

{

return n ? ((n & 1) ? a*Deg(a*a, n >> 1) : Deg(a*a, n >> 1)) : 1;

204

}

Пример 11. Требуется в целочисленной переменной a, занимающей не менее два байта памяти, поменять местами последний (младший) и предпоследний байты.

a = (a & ~0xFFFF) | ((a & 0xFF) << 8) | ((a >> 8) & 0xFF);

Пример 12. Пусть имеется массив целых чисел a размера n, состоящий из попарно различных элементов. Вывести на экран всевозможные подмножества элементов, входящие в состав массива a.

Для написания алгоритма решения данной задачи заметим следующее. Пусть A = {a1, …, an} – некоторое множество. Тогда любому подмножеству B множества A взаимно однозначно соответствует двоичный вектор vB = (v1, …, vn), в котором vi =1 тогда и только тогда, когда ai принадлежит множеству B. Всего таких различных двоичных векторов длины n ровно 2n. Все данные двоичные векторы длины n суть двоичная запись чисел от 0 до 2n-1. Поэтому алгоритм получается следующим.

#include<stdio.h>

void Print(int *a, int size, int v)

{

int i;

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

{

if (v & 1) printf("%d ", a[i]);

v >>= 1;

}

printf("\n");

}

void Subsets(int *a, int size)

{

205