четверг, 4 декабря 2014 г.

Циклы while и for

В цикле

while (выражение)
    инструкция


вычисляется выражение. Если его значение отлично от нуля, то выполняется инструкция, и вычисление выражения повторяется. Этот цикл продолжается до тех пор, пока выражение не станет равным нулю, после чего вычисления продолжатся с точки, расположенной сразу за инструкцией.

Инструкция for

for (выр1; выр2; выр3)
инструкция


эквивалентна конструкции

выр1;
while (выр2) {
    инструкция
    выр3;
}

если не считать отличий в поведении инструкции continue.

С точки зрения грамматики три компоненты цикла for представляют собой произвольные выражения, но чаще выр1 и выр3 — это присваивания или вызовы функций, а выр2 — выражение отношения. Любое из этих трех выражений может отсутствовать, но точку с запятой опускать нельзя. При отсутствии выр1 или выр3 считается, что их просто нет в конструкции цикла; при отсутствии выр2 предполагается, что его значение как бы всегда истинно. Например,

for (;;) {
...
}

есть "бесконечный" цикл, выполнение которого, вероятно, прерывается каким-то другим способом, например с помощью инструкций break или return.

Какой цикл выбрать: while или for — это дело вкуса. Так, в

while ((с = getchar()) == ' ' || с == '\n' || с == '\t')
    ; /* обойти символы-разделители */

нет ни инициализации, ни пересчета параметра, поэтому здесь больше подходит while.

Там, где есть простая инициализация и пошаговое увеличение значения некоторой переменной, больше подходит цикл for, так как в этом цикле организующая его часть сосредоточена в начале записи. Например, начало цикла, обрабатывающего первые n элементов массива, имеет следующий вид:

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

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

В качестве более внушительного примера авторы книги приводят другую версию программы atoi, выполняющей преобразование строки в ее числовой эквивалент. Это более общая версия по сравнению с рассмотренной ранее, в том смысле, что она игнорирует левые символы-разделители (если они есть) и должным образом реагирует на знаки + и -, которые могут стоять перед цифрами.

Структура программы отражает вид вводимой информации:

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

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

#include <stdio.h>
#include <ctype.h>
#define MAXLINE 1000    /* максимальный размер вводимой строки */

int getstr(char line[], int maxline);
int atoi(char line[]);

int main(void)
{
int len = 0;
int number = 0;
char line[MAXLINE]; /* текущая строка */

while ((len = getstr(line, MAXLINE)) > 0)
{
number = atoi(line);
printf("Преобразует в число %d\n", number);
number = 0;
}

return 0;
}

/* getline: читает строку в s, возвращает длину */
int getstr(char s[], int lim)
{
int c, i;

for (i = 0; i < lim - 1 && (c = getchar()) != EOF && c != '\n'; ++i)
s[i] = c;
if (c == '\n')
{
s[i] = c;
++i;
}
s[i] = '\0';        /* в конец строки дописывам "0" */
return i;   /* функция возвращает длину строки */
}

/* atoi: преобразование s в целое число; версия 2 */
int atoi(char s[])
{
int i, n, sign;
/* игнорировать символы-разделители */
for (i = 0; isspace(s[i]); i++)
;
sign = (s[i] == '-') ? -1 : 1;
if (s[i] == '+' || s[i] == '-') /* пропуск знака */
i++;
for (n = 0; isdigit(s[i]); i++)
n = 10 * n + (s[i] - '0');
return sign * n;
}

В стандартной библиотеке имеется более совершенная функция преобразования строки в длинное целое (long int) — функция strtol.


Преимущества, которые дает централизация управления циклом, становятся еще более очевидными, когда несколько циклов вложены друг в друга. Проиллюстрируем их на примере сортировки массива целых чисел методом Шелла, предложенным им в 1959 г. Основная идея этого алгоритма в том, что на ранних стадиях сравниваются далеко отстоящие друг от друга, а не соседние элементы, как в обычных перестановочных сортировках. Это приводит к быстрому устранению массовой неупорядоченности, благодаря чему на более поздней стадии остается меньше работы. Интервал между сравниваемыми элементами постепенно уменьшается до единицы, и в этот момент сортировка сводится к обычным перестановкам соседних элементов. Программа shellsort имеет следующий вид:

#include <stdio.h>
#include <stdlib.h> /* избавление от предупреждений rand, srand */
#include <time.h>

#define MAX 10

void shellsort(int v[], int n);
int binsearch(int x, int v[], int n);

int main(void)
{

int index = 0;
int mass[MAX];
int pos;  /* позиция искомого значения в массиве */
int el = 6;  /* искомое значение */
int k;

/* инициализация текущего времени в качестве "семени" */
srand((unsigned)time(NULL));

for (index = 0; index < MAX; index++)
{
mass[index] = rand() % 100; /* генерация случайных элементов массива от 0 до 100*/
}

for (k = 0; k < MAX; k++)
printf("%2d ", mass[k]);

shellsort(mass, MAX); /* сортировка элементов массива методом Шелла */

printf("\n");
for (k = 0; k < MAX; k++)
printf("%2d ", mass[k]);

pos = binsearch(el, mass, index);
printf("\nЗначение %d имеет индекс %d\n", el, pos);


return 0;
}

/* shellsort: сортируются v[0] ... v[n-1] в возрастающем порядке */
void shellsort(int v[], int n)
{
int gap, i, j, temp;
for (gap = n / 2; gap > 0; gap /= 2)
for (i = gap; i < n; i++)
for (j = i - gap; j >= 0 && v[j] > v[j + gap]; j -= gap) {
temp = v[j];
v[j] = v[j + gap];
v[j + gap] = temp;
}
}

/* binsearch: найти x в v[0] <= v[1] <= ... <= v[n-1] */
int binsearch(int x, int v[], int n)
{
int low, high, mid;

low = 0;
high = n - 1;
while (low <= high) {
mid = (low + high) / 2;
if (x < v[mid])
high = mid - 1;
else if (x > v[mid])
low = mid + 1;
else  /* совпадение найдено */
return mid;
}

return -1;  /* совпадения нет */
}

Здесь использованы три вложенных друг в друга цикла. Внешний управляет интервалом gap между сравниваемыми элементами, сокращая его путем деления пополам от n/2 до нуля. Средний цикл перебирает элементы. Внутренний — сравнивает каждую пару элементов, отстоящих друг от друга на расстоянии gap, и переставляет элементы в неупорядоченных парах. Так как gap обязательно сведется к единице, все элементы в конечном счете будут упорядочены. Следует обратить внимание на то, что универсальность цикла for позволяет сделать внешний цикл по форме похожим на другие, хотя он и не является арифметической прогрессией.

Последний оператор Си — это "," (запятая), которую чаще всего используют в инструкции for. Пара выражений, разделенных запятой, вычисляется слева направо. Типом и значением результата являются тип и значение правого выражения, что позволяет в инструкции for в каждой из трех компонент иметь по нескольку выражений, например вести два индекса параллельно. Авторы книги демонстрируют это на примере функции reverse(s), которая "переворачивает" строку s, оставляя результат в той же строке s:

#include <stdio.h>
#include <string.h>

void reverse(char s[]);

int main(void)
{
char stroka[] = "Hello, world!";
/* Вывод строки */
printf("%s\n", stroka);
reverse(stroka);
/* Вывод перевернутой строки */
printf("%s\n", stroka);

return 0;
}

/* reverse: переворачивает строку s (результат в s) */
void reverse(char s[])
{
int c, i, j;
for (i = 0, j = strlen(s) - 1; i < j; i++, j--) {
c = s[i];
s[i] = s[j];
s[j] = c;
}
}

Запятые, разделяющие аргументы функции, переменные в объявлениях и прочее не являются операторами-запятыми и не обеспечивают вычислений слева направо.

Запятыми как операторами следует пользоваться умеренно. Более всего они уместны в конструкциях, которые тесно связаны друг с другом (как в for-цикле программы reverse), а также в макросах, в которых многоступенчатые вычисления должны быть выражены одним выражением. Запятой-оператором в программе reverse можно было бы воспользоваться и при обмене символами в проверяемых парах элементов строки, мысля этот обмен как одну отдельную операцию:

/* reverse: переворачивает строку s (результат в s) */
void reverse(char s[])
{
int c, i, j;
for (i = 0, j = strlen(s) - 1; i < j; i++, j--) {
c = s[i] , s[i] = s[j], s[j] = c;
}
}

Упражнение 3.2.

Напишите функцию escape(s, t), которая при копировании текста из t в s преобразует такие символы, как новая строка и табуляция в "видимые последовательности символов" (вроде \n и \t). Используйте инструкцию switch. Напишите функцию, выполняющую обратное преобразование эскейп-последовательностей в настоящие символы.

понедельник, 1 декабря 2014 г.

Переключатель switch

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

switch (выражение) {
    case конст-выр: инструкции
    case конст-выр: инструкции
    default: инструкции
}


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

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

#include <stdio.h>

/* подсчет цифр, символов-разделителей и прочих символов */
int main(void)
{
    int c, i, nwhite, nother, ndigit[10];
    nwhite = nother = 0;
    for (i = 0; i < 10; i++)
        ndigit[i] = 0;
    while ((c = getchar()) != EOF) {
        switch (c) {
            case '0': case '1': case '2': case '3': case '4':
            case '5': case '6': case '7': case '8': case '9':
                ndigit[c - '0']++;
                break;
            case ' ':
            case '\n':
            case '\t':
                nwhite++;
                break;
            default:
                nother++;
                break;
        }
    }
    printf("Цифры =");
    for (i = 0; i < 10; i++)
        printf(" %d", ndigit[i]);
    printf(", символы-разделители = %d, прочие = %d\n", nwhite, nother);
    return 0;
}
$ cc -g -O0 -Wall -o a.out count.c
$ ./a.out
test 1234567890 123 12 1 1      1

Цифры = 1 6 3 2 1 1 1 1 1 1, символы-разделители = 7, прочие = 4


Инструкция break вызывает немедленный выход из переключателя switch. Поскольку выбор ветви case реализуется как переход на метку, то после выполнения одной ветви case, если ничего не предпринять, программа провалится вниз на следующую ветвь. Инструкции break и return — наиболее распространенные средства выхода из переключателя. Инструкция break используется также для принудительного выхода из циклов while, for и do-while.

пятница, 28 ноября 2014 г.

Упражнение 3.1.

В нашей программе бинарного поиска внутри цикла осуществляются две проверки, хотя могла быть только одна (при увеличении числа проверок вне цикла). Напишите программу, предусмотрев в ней одну проверку внутри цикла. Оцените разницу во времени выполнения.

среда, 26 ноября 2014 г.

Конструкция else-if

Последовательность инструкций

if (выражение)
    инструкция
else if (выражение)
    инструкция
else if (выражение)
    инструкция
else if (выражение)
    инструкция
else
    инструкция

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

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

else
    инструкция

можно опустить или использовать для фиксации ошибочной ("невозможной") ситуации.

В качестве иллюстрации трехпутевого ветвления авторы книги предлагают рассмотреть функцию бинарного поиска значения x в массиве v. Предполагается, что элементы v упорядочены по возрастанию. Функция выдает положение x в v (число в пределах от 0 до n-1 ), если x там встречается, и -1 , если его нет.

При бинарном поиске значение x сначала сравнивается с элементом, занимающим серединное положение в массиве v. Если x меньше, чем это значение, то областью поиска становится "верхняя" половина массива v, в противном случае — "нижняя". В любом случае следующий шаг — это сравнение с серединным элементом отобранной половины. Процесс "уполовинивания" диапазона продолжается до тех пор, пока либо не будет найдено значение, либо не станет пустым диапазон поиска.
$ vim binsearch.c
#include <stdio.h>

int binsearch(int x, int v[], int n);

int main(void)
{
    int index = 10;
    int arr[10] = { 1, 3, 4, 7, 15, 18, 33, 34, 67, 88 }; /* упорядоченный по возрастанию массив из 10 элементов */
    int pos;  /* позиция искомого значения в массиве */
    int el = 7;  /* искомое значение */

    pos = binsearch(el, arr, index);
    printf("Значение %d имеет индекс %d\n", el, pos);

    return 0;
}

/* binsearch: найти x в v[0] <= v[1] <= ... <= v[n-1] */
int binsearch(int x, int v[], int n)
{
    int low, high, mid;

    low = 0;
    high = n - 1;
    while (low <= high) {
        mid = (low + high) / 2;
        if (x < v[mid])
            high = mid - 1;
        else if (x > v[mid])
            low = mid + 1;
        else  /* совпадение найдено */
            return mid;
    }

    return -1;  /* совпадения нет */
}
$ cc -g -O0 -Wall -o a.out binsearch.c
$ ./a.out
Значение 7 имеет индекс 3

Основное действие, выполняемое на каждом шаге поиска, — сравнение значения x (меньше, больше или равно) с элементом v[mid]; это сравнение естественно поручить конструкции else-if.

понедельник, 24 ноября 2014 г.

Конструкция if-else

Инструкция if-else используется для принятия решения. Формально ее синтаксисом является:

if (выражение)
    инструкция1
else
    инструкция2

причем else-часть может и отсутствовать. Сначала вычисляется выражение, и, если оно истинно (т. е. отлично от нуля), выполняется инструкция1. Если выражение ложно (т. е. его значение равно нулю) и существует else-часть, то выполняется инструкция2.

Так как if просто проверяет числовое значение выражения, условие иногда можно записывать в сокращенном виде. Так, запись

if (выражение)
короче, чем

if (выражение != 0)

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

if (n > 0)
    if (а > Ь)
        z = а;
    else
        z = b;


else относится к внутреннему if, что и показано с помощью отступов.

Если требуется иная интерпретация, необходимо должным образом расставить фигурные скобки:

if (n > 0) {
    if (a > b)
        z = а;
    }
else
    z = b;

По правилам грамматики за if должна следовать инструкция, а выражение-инструкция вроде z = а; всегда заканчивается точкой с запятой.

Инструкции и блоки

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

Выражения, х = 0, или i++, или printf (...) , становятся инструкциями, если в конце них поставить точку с запятой, например:

х = 0;

i++;

printf (...);

В Си точка с запятой является заключающим символом инструкции.

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

Фигурные скобки, обрамляющие группу инструкций, образующих тело функции, — это один пример; второй пример — это скобки, объединяющие инструкции, помещенные после if, else, while или for.

пятница, 21 ноября 2014 г.

Приоритет и очередность вычислений


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

Таблица 1. Приоритеты и очередность вычислений операторов


Операторы Выполняются
() [] -> . слева направо
! ~ ++ -- + - * & (type) sizeof справа налево
* / % слева направо
+ - слева направо
<< >> слева направо
< <= > >= слева направо
== != слева направо
& слева направо
^ слева направо
| слева направо
&& слева направо
|| слева направо
?: справа налево
= += -= *= /= %= &= ^= |= <<= >>= справа налево
, слева направо

Примечание. Унарные операторы +, -, * и & имеют более высокий приоритет, чем те же бинарные операторы.

Приоритеты побитовых операторов &, ^ и | ниже, чем приоритет == и !=, из-за чего в побитовых проверках, таких как

if ((х & MASK) == 0) ...

чтобы получить правильный результат, необходимо использовать скобки.

В языке Си очередность вычисления операндов оператора не фиксируется. В инструкции

х = f() + g();

f может быть вычислена раньше g или наоборот. Из этого следует, что если одна из функций изменяет значение переменной, от которой зависит другая функция, то помещаемый в x результат может зависеть от очередности вычислений. Чтобы обеспечить нужную последовательность вычислений, промежуточные результаты можно запоминать во временных переменных.

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

printf("%d %d\n", ++n, power(2, n)); /* НЕВЕРНО */
может давать несовпадающие результаты. Результат вызова функции зависит от того, когда компилятор сгенерирует команды увеличения n — до или после обращения к power. Чтобы обезопасить себя от возможного побочного эффекта, достаточно написать

++n;
printf("%d %d\n", n, power(2, n));

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

a[i] = i++;

возникает вопрос: массив a индексируется старым или измененным значением i?

Авторы книги рекомендуют не писать программы, зависящие от очередности вычислений, так как компиляторы могут по-разному их генерировать в зависимости от архитектуры машины.

Упражнение 2.10.

Напишите функцию lower, которая переводит большие буквы в малые, используя условное выражение (а не конструкцию if-else).

Условные выражения

Инструкции

if (а > b)
    z = a;
else
    z = b;

пересылают в z большее из двух значений a и b. Условное выражение, написанное с помощью тернарного (т.е. имеющего три операнда) оператора ?:, представляет собой другой способ записи этой и подобных ей конструкций:

z = (a > b) ? а : b; /* z = max(a, b) */

В выражении

выр1 ? выр2 : выр3

первым вычисляется выражение выр1. Если его значение не нуль (истина), то вычисляется выражение выр2 и значение этого выражения становится значением всего условного выражения. В противном случае вычисляется выражение выр3 и его значение становится значением условного выражения. Следует отметить, что из выражений выр2 и выр3 вычисляется только одно из них.

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

Если выр2 и выр3 принадлежат разным типам, то тип результата определяется правилами преобразования, о которых шла речь ранее. Например, если f имеет тип float, a n — тип int, то типом выражения

(n > 0) ? f : n

будет float вне зависимости от того, положительно значение n или нет.

Заключать в скобки первое выражение в условном выражении не обязательно, так как приоритет ?: очень низкий (более низкий приоритет имеет только присваивание), но авторы книги рекомендуют всегда это делать, для лучшего восприятия.

В качестве примера приведится цикл, обеспечивающий печать n элементов массива по 10 на каждой строке с одним пробелом между колонками; каждая строка цикла, включая последнюю, заканчивается символом новой строки:

#include <stdio.h>

int main(void)
{
    int i, n;
    char a[] = "C is a general-purpose programming language initially developed by Dennis Ritchie.";
    n = sizeof(a);

    for (i = 0; i < n; i++)
        printf("%6d%c", a[i], (i % 10 == 9 || i == n - 1) ? '\n' : ' ');
    return 0;
}

Символ новой строки посылается после каждого десятого и после n-го элемента. За всеми другими элементами следует пробел.

Еще один пример:

#include <stdio.h>
int main(void)
{
    int n;
    char a[] = "Привет, мир!";
    n = sizeof(a);

    printf("Вы имеете %d элемент%s.\n", n, (n % 10 == 1 && n % 100 != 11) ?
    " " : ((n % 100 < 10 || n % 100 > 20) && n % 10 >= 2 && n % 10 <= 4) ?
    "а" : "ов");

    return 0;
}

Упражнение 2.9.

Применительно к числам, в представлении которых использован дополнительный код, выражение х &= (х-1) уничтожает самую правую 1 в х. Объясните, почему. Используйте это наблюдение при написании более быстрого варианта функции bitcount.

Операторы и выражения присваивания

Выражение

i = i + 2

в котором стоящая слева переменная повторяется и справа, можно написать в сжатом виде:

i += 2

Оператор +=, как и =, называется оператором присваивания.

Большинству бинарных операторов (аналогичных + и имеющих левый и правый операнды) соответствуют операторы присваивания ор=, где ор — один из операторов

+  *  /  %  <<  >>  &  ^  |

Если выр1 и выр2 — выражения, то

выр1 ор= выр2

эквивалентно

выр1 = (выр1) ор (выр2)

с той лишь разницей, что выр1 вычисляется только один раз. Обратите внимание на скобки вокруг выр2:

x *= y + 1

эквивалентно

x = x * (y + 1)

но не

x = x * y + 1

В качестве примера авторы книги приводят функцию bitcount, подсчитывающую число единичных битов в своем аргументе целочисленного типа:

#include <stdio.h>

int bitcount(unsigned x);

int main(void)
{
printf("%u\n", bitcount(100));
return 0;
}

/* bitcount: подсчет единиц в х */
int bitcount(unsigned x)
{
int b;
for (b = 0; x != 0; x >>= 1)
if (x & 01)
b++;
return b;
}

Независимо от машины, на которой будет работать эта программа, объявление аргумента x как unsigned гарантирует, что при правом сдвиге освобождающиеся биты будут заполняться нулями, а не знаковым битом.

Кроме того, в сложных выражениях вроде

yyval[yypv[p3 + p4] + yypv[p1 + p2]] += 2

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

Присваивание вырабатывает значение и может применяться внутри выражения:

while ((с = getchar()) != EOF)
    ...
В выражениях встречаются и другие операторы присваивания (+=, -= и т.д.), хотя и реже.

Типом и значением любого выражения присваивания являются тип и значение его левого операнда после завершения присваивания.

среда, 19 ноября 2014 г.

Упражнение 2.8.

Напишите функцию rightrot(х, n), которая циклически сдвигает x вправо на n разрядов.

Упражнение 2.7.

Напишите функцию invert(х, р, n), возвращающую значение x с инвертированными n битами, начиная с позиции p (остальные биты не изменяются).

Упражнение 2.6.

Напишите функцию setbits(x, p, n, y), возвращающую значение x, в котором n битов, начиная с p-й позиции, заменены на n правых разрядов из y (остальные биты не изменяются).

четверг, 13 ноября 2014 г.

Побитовые операторы

В Си имеются шесть операторов для манипулирования с битами. Их можно применять только к целочисленным операндам, т. е. к операндам типов char, short, int и long, знаковым и беззнаковым.

& — побитовое И.
| — побитовое ИЛИ.
^ — побитовое исключающее ИЛИ.
<< — сдвиг влево.
>> — сдвиг вправо.
~ — побитовое отрицание (унарный).

Оператор & (побитовое И) часто используется для обнуления некоторой группы разрядов.
Например

n = n & 0177;

обнуляет в n все разряды, кроме младших семи.

Число 0177 записано в восьмеричной системе. В двоичной системе это число равно
0000 0000 0000 0000 0000 0000 0111 1111, размер целого на моей машине 4 байта.
Пусть целая переменная n = 65535 
(0000 0000 0000 0000 1111 1111 1111 1111 в двоичном виде)

0000 0000 0000 0000 1111 1111 1111 1111
&
0000 0000 0000 0000 0000 0000 0111 1111
---------------------------------------
0000 0000 0000 0000 0000 0000 0111 1111

#include <stdio.h>
int main(void)
{
    int n = 65535;
    n = n & 0177;
    
    printf("%d\n", n);
}

Восьмеричные числа используются из-за удобства при работе с двоичной системой счисления. Каждая цифра восьмеричного числа может быть заменена тремя цифрами двоичного.
Таблица соответствия цифр восьмеричной системы счисления числам двоичной системы:

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

Оператор | (побитовое ИЛИ) применяют для установки разрядов; так,

х = х | SET_ON;

устанавливает единицы в тех разрядах х, которым соответствуют единицы в SET_ON.

#include <stdio.h>
#define SET_ON 07 /* 0000 0000 0000 0000 0000 0000 0000 0111 */

int main(void)
{
int n = 120; /* 0000 0000 0000 0000 0000 0000 0111 1000 */
n = n | SET_ON;
printf("%d\n", n);
}

0000 0000 0000 0000 0000 0000 0000 0111
|
0000 0000 0000 0000 0000 0000 0111 1000
---------------------------------------
0000 0000 0000 0000 0000 0000 0111 1111

Оператор ^ (побитовое исключающее ИЛИ) в каждом разряде установит 1, если соответствующие разряды операндов имеют различные значения, и 0, когда они совпадают.

#include <stdio.h>

int main(void)
{
int a = 017; /* 0000 0000 0000 0000 0000 0000 0000 1111 */
int b = 036; /* 0000 0000 0000 0000 0000 0000 0001 1110 */
int c = 0;
c = a ^ b;

printf("%o ^ %o = %o\n", a, b, c);
printf("%x ^ %x = %x\n", a, b, c);
}
17 ^ 36 = 21
f ^ 1e = 11

0000 0000 0000 0000 0000 0000 0000 1111
^
0000 0000 0000 0000 0000 0000 0001 1110
---------------------------------------
0000 0000 0000 0000 0000 0000 0001 0001

Операторы << и >> сдвигают влево или вправо свой левый операнд на число битовых позиций, задаваемое правым операндом, который должен быть неотрицательным. Так, х << 2 сдвигает значение х влево на 2 позиции, заполняя освобождающиеся биты нулями, что эквивалентно умножению х на 4. Сдвиг вправо беззнаковой величины всегда сопровождается заполнением освобождающихся разрядов нулями.

#include <stdio.h>

int main(void)
{
unsigned char a = 11; /* 0000 1011 */
a = a << 2;

printf("The result is %d\n", a);
}
The result is 44

0000 1011    11
<< 2         *4
0010 1100    44

Как видно из примера, освободившиеся биты справа заполнились нулями.

#include <stdio.h>

int main(void)
{
unsigned char a = 11; /* 0000 1011 */
a = a >> 2;

printf("The result is %d\n", a);
}
The result is 2

0000 1011       11
>> 2            /4
0000 0010 11    2

Сдвиг числа на 2 разряда вправо равносилен его делению на 4. Выдвигаемые наружу разряды содержат остаток от деления (11 в двоичной системе = 3 в десятичной; 11 % 4 = 3).

Сдвиг вправо знаковой величины на одних машинах происходит с распространением знака ("арифметический сдвиг"), на других — с заполнением освобождающихся разрядов нулями ("логический сдвиг").

#include <stdio.h>

int main(void)
{
signed char a = -16; /* 1111 0000 */
a = a >> 2;

printf("The result is %d\n", a);
}
The result is -4

1111 0000    -16
>> 2         /4
1111 1100    -4

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

Унарный оператор ~ поразрядно "обращает" целое т. е. превращает каждый единичный бит в нулевой и наоборот. Например
х = х & ~077
обнуляет в х последние 6 разрядов. Заметим, что запись х & ~077 не зависит от длины слова, и, следовательно, она лучше, чем х & 0177700, поскольку последняя подразумевает, что х занимает 16 битов. Не зависимая от машины форма записи ~077 не потребует дополнительных затрат при счете, так как ~077 — константное выражение, которое будет вычислено во время компиляции.

 077 - 0000 0000 0011 1111
~077 - 1111 1111 1100 0000 - 177700 (2 байта)
1111 1111 1111 1111 1111 1111 1100 0000 - 37777777700 (4 байта)


Для иллюстрации некоторых побитовых операций авторы книги предлагают рассмотреть функцию getbits(x, p, n), которая формирует поле в n битов, вырезанных из х, начиная с позиции p, прижимая его к правому краю. Предполагается, что 0-й бит — крайний правый бит, а n и p — осмысленные положительные числа. Например, getbits(x, 4, 3) вернет в качестве результата 4, 3 и 2-й биты значения х, прижимая их к правому краю:

#include <stdio.h>

unsigned getbits(unsigned х, int p, int n);

int main(void)
{
printf("%u\n", getbits(126, 4, 3));

return 0;
}

/* getbits: получает n бит, начиная с р-й позиции */
unsigned getbits(unsigned х, int p, int n)
{
return (х >> (p + 1 - n)) & ~(~0 << n);
}

Выражение х >> (p+1-n) сдвигает нужное нам поле к правому краю. Константа ~0 состоит из одних единиц, и ее сдвиг влево на n бит (~0 << n) приведет к тому, что правый край этой константы займут n нулевых разрядов. Еще одна операция побитовой инверсии ~ позволяет получить справа n единиц.