Битовые операции. Поразрядные операторы. Представление отрицательных чисел

Поразрядные операторы

В C# предусмотрен ряд поразрядных операторов , расширяющих круг задач, для решения которых можно применять C#. Поразрядные операторы воздействуют на отдельные двоичные разряды (биты) своих операндов. Они определены только для целочисленных операндов, поэтому их нельзя применять к данным типа bool, float или double.

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

Поразрядные операторы И, ИЛИ, исключающее ИЛИ и НЕ

Поразрядные операторы И, ИЛИ, исключающее ИЛИ и НЕ обозначаются следующим образом: &, |, ^ и ~. Они выполняют те же функции, что и их логические аналоги . Но в отличие от логических операторов, поразрядные операторы действуют на уровне отдельных двоичных разрядов.

С точки зрения наиболее распространенного применения поразрядную операцию И можно рассматривать как способ подавления отдельных двоичных разрядов. Это означает, что если какой-нибудь бит в любом из операндов равен 0, то соответствующий бит результата будет сброшен в 0. Поразрядный оператор ИЛИ может быть использован для установки отдельных двоичных разрядов. Если в 1 установлен какой-нибудь бит в любом из операндов этого оператора, то в 1 будет установлен и соответствующий бит в другом операнде. Поразрядный оператор исключающее ИЛИ устанавливает двоичный разряд операнда в том и только в том случае, если двоичные разряды сравниваемых операндов оказываются разными, как в приведенном ниже примере. Для понимания вышесказaнного, разберите следующий пример:

Давайте теперь рассмотрим пример программы, использующей поразрядные операторы:

Using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { class Program { static void Main(string args) { chet(16); provChet(8); nechet(16); Console.ReadLine(); } // Метод, преобразующий все нечетные числа в четные // в диапазоне c помощью // поразрядного оператора & static void chet(int x) { int result; Console.WriteLine("Преобразованный диапазон чисел от 0 до {0}:\n",x); for (int i = 0; i

Операторы сдвига

В C# имеется возможность сдвигать двоичные разряды, составляющие целое значение, влево или вправо на заданную величину. Ниже приведена общая форма для этих операторов:

значение
значение >> число битов

где число_битов - это число двоичных разрядов, на которое сдвигается указанное значение.

При сдвиге влево все двоичные разряды в указываемом значении сдвигаются на одну позицию влево, а младший разряд сбрасывается в нуль. При сдвиге вправо все двоичные разряды в указываемом значении сдвигаются на одну позицию вправо. Если вправо сдвигается целое значение без знака, то старший разряд сбрасывается в нуль. А если вправо сдвигается целое значение со знаком, то разряд знака сохраняется. Напомним, что для представления отрицательных чисел старший разряд целого числа устанавливается в 1. Так, если сдвигаемое значение является отрицательным, то при каждом сдвиге вправо старший разряд числа устанавливается в 1. А если сдвигаемое значение является положительным, то при каждом сдвиге вправо старший разряд числа сбрасывается в нуль.

Разделы на этой странице:

Поразрядные операторы

В C# предусмотрен ряд поразрядных операторов, расширяющих круг задач, для решения которых можно применять С#. Поразрядные операторы воздействуют на отдельные двоичные разряды (биты) своих операндов. Они определены только для целочисленных операндов, поэтому их нельзя применять к данным типа bool, float или double.

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

Таблица 4.1. Поразрядные операторы

Оператор Значение
& Поразрядное И
| Поразрядное ИЛИ
^ Поразрядное исключающее ИЛИ
>> Сдвиг вправо
<< Сдвиг влево
~ Дополнение до 1 (унарный оператор НЕ)

Поразрядные операторы И, ИЛИ, исключающее ИЛИ и НЕ

Поразрядные операторы И, ИЛИ, исключающее ИЛИ и НЕ обозначаются следующим образом: &, |, ^ и ~. Они выполняют те же функции, что и их логические аналоги, рассмотренные выше. Но в отличие от логических операторов, поразрядные операторы действуют на уровне отдельных двоичных разрядов. Ниже приведены результаты поразрядных операций с двоичными единицами и нулями.

р q р & q p | q p ^ q ~р
0 0 0 0 0 1
1 0 0 1 1 0
0 1 0 1 1 1
1 1 1 1 0 0

С точки зрения наиболее распространенного применения поразрядную операцию И можно рассматривать как способ подавления отдельных двоичных разрядов. Это означает, что если какой-нибудь бит в любом из операндов равен 0, то соответствующий бит результата будет сброшен в 0. Например:

1101 0011
1010 1010
&_________
1000 0010

В приведенном ниже примере программы демонстрируется применение поразрядного оператора & для преобразования нечетных чисел в четные. Для этой цели достаточно сбросить младший разряд числа. Например, число 9 имеет следующий двоичный вид: 0000 1001. Если сбросить младший разряд этого числа, то оно станет числом 8, а в двоичной форме - 0000 1000.

// Применить поразрядный оператор И, чтобы сделать число четным.
using System;
class MakeEven {
static void Main() {
ushort num; ushort i;
for(i =1; i <= 10; i++) {
num = i;

num = (ushort) (num & 0xFFFE);
Console.WriteLine("num после сброса младшего разряда: "
+ num + "n");
}
}
}

num: 1
num после сброса младшего разряда: 0
num: 2

num: 3
num после сброса младшего разряда: 2
num: 4

num: 5
num после сброса младшего разряда: 4
num: 6

num: 7
num после сброса младшего разряда: 6
num: 8

num: 9
num после сброса младшего разряда: 8
num: 10
num после сброса младшего разряда: 10

Шестнадцатеричное значение 0xFFFE, используемое в поразрядном операторе И, имеет следующую двоичную форму: 1111 1111 1111 1110. Таким образом, поразрядная операция И оставляет без изменения все двоичные разряды в числовом значении переменной num, кроме младшего разряда, который сбрасывается в нуль. В итоге четные числа не претерпевают никаких изменений, а нечетные уменьшаются на 1 и становятся четными.

Поразрядным оператором И удобно также пользоваться для определения установленного или сброшенного состояния отдельного двоичного разряда. В следующем примере программы определяется, является ли число нечетным.

// Применить поразрядный оператор И, чтобы определить,
// является ли число нечетным.
using System;
class IsOdd {
static void Main() {
ushort num;
num = 10;
if((num & 1) == 1)
Console.WriteLine("He выводится.") ;
num = 11;
if((num & 1) == 1)
Console.WriteLine(num + " - нечетное число.");
}
}

Вот как выглядит результат выполнения этой программы.

11 - нечетное число.

В обоих операторах if из приведенной выше программы выполняется поразрядная операция И над числовыми значениями переменной num и 1. Если младший двоичный разряд числового значения переменной num установлен, т.е. содержит двоичную 1, то результат поразрядной операции num & 1 оказывается равным 1. В противном случае он равен нулю. Поэтому оператор i f может быть выполнен успешно лишь в том случае, если проверяемое число оказывается нечетным.

Возможностью проверять состояние отдельных двоичных разрядов с помощью поразрядного оператора & можно воспользоваться для написания программы, в которой отдельные двоичные разряды проверяемого значения типа byte приводятся в двоичной форме. Ниже показан один из способов написания такой программы.

// Показать биты, составляющие байт.
using System;
class ShowBits {
static void Main() {
int t;
byte val;
val = 123;
for(t=128; t > 0; t = t/2) {


}
}
}

Выполнение этой программы дает следующий результат.

0 1 1 1 1 0 1 1

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

Поразрядный оператор ИЛИ может быть использован для установки отдельных двоичных разрядов. Если в 1 установлен какой-нибудь бит в любом из операндов этого оператора, то в 1 будет установлен и соответствующий бит в другом операнде. Например:

1101 0011
1010 1010
|_________
1111 1011

Используя поразрядный оператор ИЛИ, можно без особого труда превратить упоминавшийся выше пример программы, преобразующей нечетные числа в четные, в приведенный ниже обратный пример, где четные числа преобразуются в нечетные.

// Применить поразрядный оператор ИЛИ, чтобы сделать число нечетным.
using System;
class MakeOdd {
static void Main() {
ushort num;
ushort i;
for(i = 1; i <= 10; i++) {
num = i;
Console.WriteLine("num: " + num);
num = (ushort) (num | 1);
Console.WriteLine("num после установки младшего разряда: "
+ num + "n");
}
}
}

Результат выполнения этой программы выглядит следующим образом.

num: 1
num после установки младшего разряда: 1
num: 2

num: 3
num после установки младшего разряда: 3
num: 4

num: 5
num после установки младшего разряда: 5
num: 6

num: 7
num после установки младшего разряда: 7
num: 8

num: 9
num после установки младшего разряда: 9
num: 10
num после установки младшего разряда: 11

В приведенной выше программе выполняется поразрядная операция ИЛИ над каждым числовым значением переменной num и 1, поскольку 1 дает двоичное значение, в котором установлен младший разряд. В результате поразрядной операции ИЛИ над 1 и любым другим значением младший разряд последнего устанавливается, тогда как все остальные разряды остаются без изменения. Таким образом, результирующее числовое значение получается нечетным, если исходное значение было четным.

Поразрядный оператор исключающее ИЛИ устанавливает двоичный разряд операнда в том и только в том случае, если двоичные разряды сравниваемых операндов оказываются разными, как в приведенном ниже примере.

0111 1111
1011 1001
^_________
1100 0110

У поразрядного оператора исключающее ИЛИ имеется одно интересное свойство, которое оказывается полезным в самых разных ситуациях. Так, если выполнить сначала поразрядную операцию исключающее ИЛИ одного значения X с другим значением Y, а затем такую же операцию над результатом предыдущей операции и значением Y, то вновь получится первоначальное значение X. Это означает, что в приведенном ниже фрагменте кода

R1 = X ^ Y;
R2 = R1 ^ Y;

значение переменной R2 оказывается в итоге таким же, как и значение переменной X. Следовательно, в результате двух последовательно выполняемых поразрядных операций исключающее ИЛИ, в которых используется одно и то же значение, получается первоначальное значение. Этим свойством данной операции можно воспользоваться для написания простой программы шифрования, в которой некоторое целое значение служит в качестве ключа для кодирования и декодирования сообщения с помощью операции исключающее ИЛИ над символами этого сообщения. В первый раз операция исключающее ИЛИ выполняется для кодирования открытого текста в зашифрованный, а второй раз - для декодирования зашифрованного текста в открытый. Разумеется, такое шифрование не представляет никакой практической ценности, поскольку оно может быть легко разгадано. Тем не менее оно служит интересным примером для демонстрации результатов применения поразрядных операторов исключающее ИЛИ, как в приведенной ниже программе.

// Продемонстрировать применение поразрядного оператора исключающее ИЛИ.

using System;
class Encode {
static void Main() {
char ch1 = "H";
char ch2 = "i" ;
char ch3 = "!";
int key = 88;
Console.WriteLine("Исходное сообщение: " + ch1 + ch2 + ch3) ;
// Зашифровать сообщение,
ch1 = (char) (ch1 ^ key);
ch2 = (char) (ch2 ^ key);
ch3 = (char) (ch3 ^ key);
Console.WriteLine("Зашифрованное сообщение: " + ch1 + ch2 + ch3);
// Расшифровать сообщение.
ch1 = (char) (ch1 ^ key);
ch2 = (char) (ch2 ^ key);
ch3 = (char) (ch3 ^ key);
Console.WriteLine("Расшифрованное сообщение: " + ch1 + ch2 + ch3);
}
}

Исходное сообщение: Hi!
Зашифрованное сообщение: Qly
Расшифрованное сообщение: Hi!

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

Поразрядный унарный оператор НЕ (или оператор дополнения до 1) изменяет на обратное состояние всех двоичных разрядов операнда. Так, если некоторое целое значение А имеет комбинацию двоичных разрядов 1001 0110, то в результате поразрядной операции ~А получается значение с комбинацией двоичных разрядов 0110 1001.

В следующем примере программы демонстрируется применение поразрядного оператора НЕ с выводом некоторого числа и его дополнения до 1 в двоичном коде.

// Продемонстрировать применение поразрядного унарного оператора НЕ.
using System;
class NotDemo {
static void Main() {
sbyte b = -34;
for(int t=128; t > 0; t = t/2) {

}
Console.WriteLine();
// обратить все биты b = (sbyte) ~b;
b = (sbyte) ~b;
for(int t=128; t > 0; t = t/2) {
if((b & t) != 0) Console.Write("1 ");
if((b & t) == 0) Console.Write("0 ");
}
}
}

Результат выполнения этой программы приведен ниже.

1 1 0 1 1 1 1 0
0 0 1 0 0 0 0 1

Операторы сдвига

В C# имеется возможность сдвигать двоичные разряды, составляющие целое значение, влево или вправо на заданную величину. Для этой цели в C# определены два приведенных ниже оператора сдвига двоичных разрядов.

<< Сдвиг влево
>> Сдвиг вправо

Ниже приведена общая форма для этих операторов:

значение « число_битов
значение » число_битов

где число_битов - это число двоичных разрядов, на которое сдвигается указанное значение.

При сдвиге влево все двоичные разряды в указываемом значении сдвигаются на одну позицию влево, а младший разряд сбрасывается в нуль. При сдвиге вправо все двоичные разряды в указываемом значении сдвигаются на одну позицию вправо. Если вправо сдвигается целое значение без знака, то старший разряд сбрасывается в нуль. А если вправо сдвигается целое значение со знаком, то разряд знака сохраняется. Напомним, что для представления отрицательных чисел старший разряд целого числа устанавливается в 1. Так, если сдвигаемое значение является отрицательным, то при каждом сдвиге вправо старший разряд числа устанавливается в 1. А если сдвигаемое значение является положительным, то при каждом сдвиге вправо старший разряд числа сбрасывается в нуль.

При сдвиге влево и вправо крайние двоичные разряды теряются. Восстановить потерянные при сдвиге двоичные разряды нельзя, поскольку сдвиг в данном случае не является циклическим.

Ниже приведен пример программы, наглядно демонстрирующий действие сдвига влево и вправо. В данном примере сначала задается первоначальное целое значение, равное 1. Это означает, что младший разряд этого значения установлен. Затем это целое значение сдвигается восемь раз подряд влево. После каждого сдвига выводятся восемь младших двоичных разрядов данного значения. Далее процесс повторяется, но на этот раз 1 устанавливается на позиции восьмого разряда, а по существу, задается целое значение 128, которое затем сдвигается восемь раз подряд вправо.

// Продемонстрировать применение операторов сдвига.
using System;
class ShiftDemo {
static void Main() {
int val = 1;
for(int i = 0; i < 8; i++) {
for(int t=128; t > 0; t = t/2) {
if((val & t) != 0) Console.Write("1 ");
if((val & t) == 0) Console.Write("0 ");
}
Console.WriteLine();
val = val << 1; // сдвиг влево
}
Console.WriteLine() ;
val = 128;
for(int i = 0; i < 8; i++) {
for(int t=128; t > 0; t = t/2) {
if((val & t) != 0) Console.Write("1 ");
if((val & t) == 0) Console.Write("0 ");
}
Console.WriteLine();
val = val >>1; // сдвиг вправо
}
}
}

Результат выполнения этой программы выглядит следующим образом.

0 0 0 0 0 0 0 1
0 0 0 0 0 0 1 0
0 0 0 0 0 1 0 0
0 0 0 0 1 0 0 0
0 0 0 1 0 0 0 0
0 0 1 0 0 0 0 0
0 1 0 0 0 0 0 0
1 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0
0 1 0 0 0 0 0 0
0 0 1 0 0 0 0 0
0 0 0 1 0 0 0 0
0 0 0 0 1 0 0 0
0 0 0 0 0 1 0 0
0 0 0 0 0 0 1 0
0 0 0 0 0 0 0 1

Двоичные разряды соответствуют форме представления чисел в степени 2, и поэтому операторы сдвига могут быть использованы для умножения или деления целых значений на 2. Так, при сдвиге вправо целое значение удваивается, а при сдвиге влево - уменьшается наполовину. Разумеется, все это справедливо лишь в том случае, если крайние разряды не теряются при сдвиге в ту или иную сторону. Ниже приведен соответствующий пример.

// Применить операторы сдвига для умножения и деления на 2.
using System;
class MultDiv {
static void Main() {
int n;
n = 10;
Console.WriteLine("Значение переменной n: " + n) ;
// Умножить на 2.
n = n << 1;
"операции n = n * 2: " + n) ;
// Умножить на 4.
n = n << 2;
Console.WriteLine("Значение переменной n: после " +
"операции n = n * 4: " + n) ;
// Разделить на 2.
n = n >> 1;
Console.WriteLine("Значение переменной n: после " +
"операции n = n / 2: " + n) ;
// Разделить на 4.
n = n >> 2;
Console.WriteLine("Значение переменной n: после " +
"операции n = n / 4: " + n) ;

Console.WriteLine();

// Установить переменную n в исходное состояние,
n = 10;
Console.WriteLine("Значение переменной n: " + n);
// Умножить на 2 тридцать раз.
n = n << 30; // данные теряются
Console.WriteLine("Значение переменной n после " +
"сдвига на 30 позиций влево: " + n);
}
}

Ниже приведен результат выполнения этой программы.

Значение переменной n: 10
Значение переменной n: после операции n = n * 2: 20
Значение переменной n: после операции n = n * 4: 80
Значение переменной n: после операции n = n / 2: 40
Значение переменной n: после операции n = n / 4: 10
Значение переменной n: 10
Значение переменной n после сдвига на 30 позиций влево: -2147483648

Обратите внимание на последнюю строку приведенного выше результата. Когда целое значение 10 сдвигается влево тридцать раз подряд, информация теряется, поскольку двоичные разряды сдвигаются за пределы представления чисел для типа int. В данном случае получается совершенно ""непригодное" значение, которое оказывается к тому же отрицательным, поскольку в результате сдвига в старшем разряде, используемом в качестве знакового, оказывается 1, а следовательно, данное числовое значение должно интерпретироваться как отрицательное. Этот пример наглядно показывает, что применять операторы сдвига для умножения или деления на 2 следует очень аккуратно. (Подробнее о типах данных со знаком и без знака см. в главе 3.)

Поразрядные составные операторы присваивания

Все двоичные поразрядные операторы могут быть использованы в составных операциях присваивания. Например, в двух приведенных ниже операторах переменной х присваивается результат выполнения операции исключающее ИЛИ над первоначальным значением переменной х и числовым значением 127.

х = х ^ 127;
х ^ = 127;

Последнее обновление: 14.09.2018

Особый класс операций представляют поразрядные операции. Они выполняются над отдельными разрядами числа. В этом плане числа рассматриваются в двоичном представлении, например, 2 в двоичном представлении 10 и имеет два разряда, число 7 - 111 и имеет три разряда.

Логические операции

    & (логическое умножение)

    Умножение производится поразрядно, и если у обоих операндов значения разрядов равно 1, то операция возвращает 1, иначе возвращается число 0. Например:

    Int x1 = 2; //010 int y1 = 5;//101 Console.WriteLine(x1&y1); // выведет 0 int x2 = 4; //100 int y2 = 5; //101 Console.WriteLine(x2 & y2); // выведет 4

    В первом случае у нас два числа 2 и 5. 2 в двоичном виде представляет число 010, а 5 - 101. Поразрядно умножим числа (0*1, 1*0, 0*1) и в итоге получим 000.

    Во втором случае у нас вместо двойки число 4, у которого в первом разряде 1, так же как и у числа 5, поэтому в итоге получим (1*1, 0*0, 0 *1) = 100, то есть число 4 в десятичном формате.

    | (логическое сложение)

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

    Int x1 = 2; //010 int y1 = 5;//101 Console.WriteLine(x1|y1); // выведет 7 - 111 int x2 = 4; //100 int y2 = 5;//101 Console.WriteLine(x2 | y2); // выведет 5 - 101

    ^ (логическое исключающее ИЛИ)

    Также эту операцию называют XOR, нередко ее применяют для простого шифрования:

    Int x = 45; // Значение, которое надо зашифровать - в двоичной форме 101101 int key = 102; //Пусть это будет ключ - в двоичной форме 1100110 int encrypt = x ^ key; //Результатом будет число 1001011 или 75 Console.WriteLine("Зашифрованное число: " +encrypt); int decrypt = encrypt ^ key; // Результатом будет исходное число 45 Console.WriteLine("Расшифрованное число: " + decrypt);

    Здесь опять же производятся поразрядные операции. Если у нас значения текущего разряда у обоих чисел разные, то возвращается 1, иначе возвращается 0. Таким образом, мы получаем из 9^5 в качестве результата число 12. И чтобы расшифровать число, мы применяем ту же операцию к результату.

    ~ (логическое отрицание или инверсия)

    Еще одна поразрядная операция, которая инвертирует все разряды: если значение разряда равно 1, то оно становится равным нулю, и наоборот.

    Int x = 12; // 00001100 Console.WriteLine(~x); // 11110011 или -13

Представление отрицательных чисел

Для записи чисел со знаком в C# применяется дополнительный код (two’s complement), при котором старший разряд является знаковым. Если его значение равно 0, то число положительное, и его двоичное представление не отличается от представления беззнакового числа. Например, 0000 0001 в десятичной системе 1.

Если старший разряд равен 1, то мы имеем дело с отрицательным числом. Например, 1111 1111 в десятичной системе представляет -1. Соответственно, 1111 0011 представляет -13.

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

Int x = 12; int y = ~x; y += 1; Console.WriteLine(y); // -12

Операции сдвига

Операции сдвига также производятся над разрядами чисел. Сдвиг может происходить вправо и влево.

    x<

    x>>y - сдвигает число x вправо на y разрядов. Например, 16>>1 сдвигает число 16 (которое в двоичном представлении 10000) на один разряд вправо, то есть в итоге получается 1000 или число 8 в десятичном представлении.

Таким образом, если исходное число, которое надо сдвинуть в ту или другую строну, делится на два, то фактически получается умножение или деление на два. Поэтому подобную операцию можно использовать вместо непосредственного умножения или деления на два.

Теги: Си битовые операции, побитовые операции, побитовое сложение, побитовое умножение, битовый сдвиг влево, битовый сдвиг вправо

Введение

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

Побитовые операции, как понятно из названия, позволяют оперировать непосредственно с битами. Большое количество примеров использования побитовых операций можно найти, например, в книге Генри Уоррена «Алгоритмические трюки для программистов». Здесь мы рассмотрим только сами операции и примитивные алгоритмы.

Побитовые И, ИЛИ, НЕ, исключающее ИЛИ

Н апомню для начала, что логические операции И, ИЛИ, исключающее ИЛИ и НЕ могут быть описаны с помощью таблиц истинности

Логический оператор НЕ
X NOT X
0 1
1 0

В побитовых (bit-wise) операциях значение бита, равное 1, рассматривается как логическая истина, а 0 как ложь. Побитовое И (оператор &) берёт два числа и логически умножает соответствующие биты. Например, если логически умножить 3 на 8, то получим 0

Char a = 3; char b = 8; char c = a & b; printf("%d", c);

Так как в двоичном виде 3 в виде однобайтного целого представляет собой

Первый бит переменной c равен логическому произведению первого бита числа a и первого бита числа b. И так для каждого бита.

00000011
00001000
↓↓↓↓↓↓↓↓
00000000

Соответственно, побитовое произведение чисел 31 и 17 даст 17, так как 31 это 00011111 , а 17 это 00010001

00011111
00010001
↓↓↓↓↓↓↓↓
00010001

Побитовое произведение чисел 35 и 15 равно 3.

00100011
00001111
↓↓↓↓↓↓↓↓
00000011

Аналогично работает операция побитового ИЛИ (оператор |), за исключением того, что она логически суммирует соответствующие биты чисел без переноса.

Например,

Char a = 15; char b = 11; char c = a | b; printf("%d", c);

выведет 15, так как 15 это 00001111 , а 11 это 00001011

00001111
00001011
↓↓↓↓↓↓↓↓
00001111

Побитовое ИЛИ для чисел 33 и 11 вернёт 43, так как 33 это 00100001 , а 11 это 00001011

00100001
00001011
↓↓↓↓↓↓↓↓
00101011

Побитовое отрицание (оператор ~) работает не для отдельного бита, а для всего числа целиком. Оператор инверсии меняет ложь на истину, а истину на ложь, для каждого бита. Например,

Char a = 65; char b = ~a; printf("%d", b);

Выведет -66, так как 65 это 01000001 , а инверсия даст 10111110

что равно -66. Кстати, вот алгоритм для того, чтобы сделать число отрицательным: для нахождение дополнительного кода числа его надо инвертировать и прибавить к нему единицу.

Char a = 107; char b = ~a + 1; printf("a = %d, -a = %d", a, b);

Исключающее ИЛИ (оператор ^) применяет побитово операцию XOR. Например, для чисел

Char a = 12; char b = 85; char c = a ^ b; printf("%d", c);

будет выведено 89, так как a равно 00001100 , а b равно 01010101 . В итоге получим 01011001

Иногда логические операторы && и || путают с операторами & и |. Такие ошибки могут существовать в коде достаточно долго, потому что такой код в ряде случаев будет работать. Например, для чисел 1 и 0. Но так как в си истиной является любое ненулевое значение, то побитовое умножение чисел 3 и 4 вернёт 0, хотя логическое умножение должно вернуть истину.

Int a = 3; int b = 4; printf("a & b = %d\n", a & b); //выведет 0 printf("a && b = %d\n", a && b);//выведет не 0 (конкретнее, 1)

Операции побитового сдвига

О пераций сдвига две – битовый сдвиг влево (оператор <<) и битовый сдвиг вправо (оператор >>). Битовый сдвиг вправо сдвигает биты числа вправо, дописывая слева нули. Битовый сдвиг влево делает противоположное: сдвигает биты влево, дописывая справа нули. Вышедшие за пределы числа биты отбрасываются.

Например, сдвиг числа 5 влево на 2 позиции

00000101 << 2 == 00010100

Сдвиг числа 19 вправо на 3 позиции

00010011 >> 3 == 00000010

Независимо от архитектуры (big-endian, или little-endian, или middle-endian) числа в двоичном виде представляются слева направо, от более значащего бита к менее значащему. Побитовый сдвиг принимает два операнда – число, над которым необходимо произвести сдвиг, и число бит, на которое необходимо произвести сдвиг.

Int a = 12; printf("%d << 1 == %d\n", a, a << 1); printf("%d << 2 == %d\n", a, a << 2); printf("%d >> 1 == %d\n", a, a >> 1); printf("%d >> 2 == %d\n", a, a >> 2);

Так как сдвиг вправо (>>) дописывает слева нули, то для целых чисел операция равносильна целочисленному делению пополам, а сдвиг влево умножению на 2. Произвести битовый сдвиг для числа с плавающей точкой без явного приведения типа нельзя. Это вызвано тем, что для си не определено представление числа с плавающей точкой. Однако можно переместить число типа float в int, затем сдвинуть и вернуть обратно

Float b = 10.0f; float c = (float) (*((unsigned int*)&b) >> 2); printf("%.3f >> 2 = %.3f", b, c);

Но мы, конечно же, получим не 5.0f, а совершенно другое число.

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

Unsigned int ua = 12; signed int sa = -11; printf("ua = %d, ua >> 2 = %d\n", ua, ua >> 2); printf("sa = %d, sa >> 2 = %d\n", sa, sa >> 2); printf("(unsigned) sa = %u, sa >> 2 = %u\n", sa, sa >> 2); printf("sa = %d, ((unsigned) sa) >> 2 = %d", sa, ((unsigned) sa) >> 2);

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

Побитовые операторы и операторы сдвига не изменяют значения числа, возвращая новое. Они также как и арифметические операторы, могут входить в состав сложного присваивания

Int a = 10; int b = 1; a >>= 3; a ^= (b << 3); и т.д.

Примеры

1. Напишем функции, которые позволяют определять и изменять определённый бит числа

Для того, чтобы узнать, какой бит (1 или 0) стоит на позиции n, воспользуемся логическим умножением.

Пусть имеется число 9

00001001

Нужно узнать, выставлен ли бит на позиции 3 (начиная с нуля). Для этого умножим его на число, у которого все биты равны нулю, кроме третьего:

00001001 & 00001000 = 00001000

Теперь узнаем значение бита в позиции 6

00001001 & 01000000 = 00000000

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

#include #include #include int checkbit(const int value, const int position) { int result; if ((value & (1 << position)) == 0) { result = 0; } else { result = 1; } return result; } void main() { int a = 3; size_t len = sizeof(int) * CHAR_BIT; size_t i; for (i = 0; i < len; i++) { printf("%d", checkbit(a, i)); } _getch(); }

Заметьте, что в функции условие записано так

(value & (1 << position)) == 0

Потому что без скобок сначала будет вычислено равенство нулю и только потом выполнено умножение.

Value & (1 << position) == 0

Функцию можно упростить

Int checkbit(const int value, const int position) { return ((value & (1 << position)) != 0); }

Функция, которая выставляет бит на n-й позиции в единицу.

Известно, что логическое сложение любого бита с 1 будет равно 1. Так что для установки n-го бита нужно логически сложить число с таким, у которого все биты, кроме нужного, равны нулю. Как получить такое число, уже рассмотрено.

Int setbit(const int value, const int position) { return (value | (1 << position)); }

Функция, которая устанавливает бит на n-й позиции в ноль.

Для этого нужно, чтобы все биты числа, кроме n-го, не изменились. Умножим число на такое, у которого все биты равны единице, кроме бита под номером n. Например

0001011 & 1110111 = 0000011

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

Int unsetbit(const int value, const int position) { return (value & ~(1 << position)); }

Функция, изменющая значение n-го бита на противоположное.

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

Int switchbit(const int value, const int position) { return (value ^ (1 << position)); }

Проверка

#include #include #include int checkbit(const int value, const int position) { return ((value & (1 << position)) != 0); } int setbit(const int value, const int position) { return (value | (1 << position)); } int unsetbit(const int value, const int position) { return (value & ~(1 << position)); } int switchbit(const int value, const int position) { return (value ^ (1 << position)); } void printbits(int n) { //CHAR_BIT опеределён в библиотеке limits.h //и хранит число бит в байте для данной платформы size_t len = sizeof(int)* CHAR_BIT; size_t i; for (i = 0; i < len; i++) { printf("%d", checkbit(n, i)); } printf("\n"); } void main() { int a = 3; size_t len = sizeof(int) * CHAR_BIT; size_t i; printbits(a); a = setbit(a, 5); printbits(a); a = unsetbit(a, 5); printbits(a); a = switchbit(a, 11); printbits(a); a = switchbit(a, 11); printbits(a); _getch(); }

Битовые флаги

Расммотрим синтетический пример. Пусть у нас есть три логические переменные, и нам нужно вывести определённое значение в зависимости от всех этих переменных сразу. Очевидно, что может быть 2 3 возможных вариантов. Запишем это условие в виде ветвления:

#include int main() { unsigned char a, b, c; a = 1; b = 0; c = 0; if (a) { if (b) { if (c) { printf("true true true"); } else { printf("true true false"); } } else { if (c) { printf("true false true"); } else { printf("true false false"); } } } else { if (b) { if (c) { printf("false true true"); } else { printf("false true false"); } } else { if (c) { printf("false false true"); } else { printf("false false false"); } } } _getch(); return 0; }

Мы получили 8 ветвей. Пусть теперь нам понадобилось добавить ещё одно условие. Тогда число ветвей удвоится, и программа станет ещё сложней для понимания и отладки. Перепишем пример.

Если каждое из наших логичесих значений сдвинуть на своё число бит влево и логически сложить, то мы получим свою уникальную комбинацию бит в зависимоти от значений a, b и c:

#include #include void printbits (int n) { int i; for (i = CHAR_BIT - 1; i >= 0; i--) { printf("%d", (n & (1 << i)) != 0); } printf("\n"); } int main() { unsigned char a, b, c; unsigned char res; a = 1; b = 0; c = 0; res = c | b << 1 | a << 2; printbits(res); a = 0; b = 1; c = 1; res = c | b << 1 | a << 2; printbits(res); a = 1; b = 0; c = 1; res = c | b << 1 | a << 2; printbits(res); _getch(); return 0; }

Используем этот подход к нашей задаче и заменим ветвеление на switch:

#include int main() { unsigned char a, b, c; unsigned char res; a = 1; b = 0; c = 0; res = c | b<< 1 | a << 2; switch (res) { case 0b00000000: printf("false false false"); break; case 0b00000001: printf("false false true"); break; case 0b00000010: printf("false true false"); break; case 0b00000011: printf("false true true"); break; case 0b00000100: printf("true false false"); break; case 0b00000101: printf("true false true"); break; case 0b00000110: printf("true true false"); break; case 0b00000111: printf("true true true"); break; } _getch(); return 0; }

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