ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 10.01.2024
Просмотров: 75
Скачиваний: 1
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
15
{ //инициализация переменных
short int a = -12345;//числовая переменная – целое число со знаком из
диапазона [−32768, +32767]
int b = 123456;//числовая переменная – длинное целое число со знаком из
диапазона [−2 147 483 648, +2 147 483 647], можно использовать long см.
limits.h
char c = -123;//числовая переменная – маленькое целое число из диапа-
зона [-127, 128]
char d[5] = "1234";//символьный массив, строка, нумерация символов с
0, в конце строки символ ноль, в тексте можно использовать d[4]=0; или
d[4]=‘\0’;
unsigned int e = 12345;//числовая переменная – целое число без знака из
диапазона [0, + 4 294 967 295]
float f = -1234.5;//числовая переменная – вещественное число с плаваю-
щей запятой в диапазоне 3.4e-38 … 3.4e+38 примерно 7 значащих цифр, dou-
ble – тоже число с плавающей точкой, примерно 15 значащих цифр, диапа-
зон 1.7e-308 … 1.7C+308
bool g = 1;// логическая переменная – значение true(1)/false(0)
bool i = false;
char n[3] = "12";
//операции над переменными
int h = a + e;//сложить две целочисленные переменные со знаком и без
знака
long j = b + c;//сложить две целочисленных переменных со знаком и без
знака разной длины
bool k = g || i;//выполнить логическое ИЛИ над переменными g и i
int v = d[3] * n[0];//умножить код (52) четвертого символа (4) массива
d на код (49) первого символа (1) массива n
//напечатать в консоли значение переменных c пояснительными ком-
ментариями
printf("Znakomstvo s Visual Studio");
printf("\n\n");
printf("h= %i", h);//как десятичное число целого типа со знаком
printf("\n");//перевод строки
printf("v= %i", v);//как десятичное число целого типа со знаком
printf("\n");//перевод строки
printf("k= %d", k);//как десятичное число целого типа со знаком
16
system("pause");//ждем действий оператора
}
Результат работы примера программы представлен на Рис. 14.
Рис. 14. Результат работы программы.
Обратите внимание, что над числовыми переменными разных типов можно выполнять математические операции, однако
необходимо учитывать
какого типа и размера может получиться результат вычислений. Так, например значение переменной «v» согласно логике работы программы полу- чилось равным 2548, а не 4.
17
3. ЭЛЕМЕНТЫ ЯЗЫКА Си
Наше восприятие текста (а числа у нас – часть текста) не делает разницы между символом и его значением. Совсем другое дело – вычислительная си- стема. Как мы выяснили там циркулирует только двоичные коды, а поскольку в наших программах встречаются различные данные, в языке Си обязательно определяются типы данных при объявлении переменных. Например:
int a,b;//объявляет целочисленные переменные а и b.
Здесь:
− разделитель, точка с запятой «;»;
− перечисление, запятая «,»;
− комментарии: «/* комментарий */» или «// комментарий до конца строки».
3.1. Типы данных
Для логических операций используется булевский тип данных BOOL, ко- торый в Си представлен целым числом и имеет два значения TRUE (1) и FALSE
(0).
Целый тип данных int – это число со знаком, которое в нашей (x86) си- стеме имеет 32 двоичных разряда. Диапазон от −2 147 483 648 до
+2 147 483 647. Из них один старший разряд играет роль знака (ведь в двоич- ном коде нет «минуса»): если значение старшего разряда 1 – это «минус», число отрицательное, если 0 – неотрицательное число.
Целые могут быть: короткие short – 16 двоичных разрядов, long – 32 дво- ичных разряда (полный аналог int, long long – 64двоичных разряда) и самые короткие char – всего один байт или 8 двоичных разрядов. Аналогия char – символ, и действительно, в простейшем случае стандартной ANSI-таблицы символов одного байта достаточно для кодирования латиницы, цифр и симво- лов нашей клавиатуры.
Текстовые константы в коде: строка, “A” – в двойных кавычках, либо один символ из строки, ‘A’ – в одиночных кавычках
Для работы с перечисляемыми данными достаточно натуральных чисел.
Тогда отпадает необходимость в использовании старшего разряда числа под знак, а диапазон значений становится от 0 до 4 294 967 295 (а комбинаций ну- лей и единиц столько же). Соответствующий тип unsigned int – беззнаковое целое, аналогично unsigned short, unsigned long long и unsigned char.
Правило: без unsigned все целые типы считаются числами со знаком.
18
Серьезный вопрос: что делает наша программа, когда выполняет действия с используемыми типами данных, например, при перемножении двух целых чисел типа int выполняется приведение результата к типу int. При этом доста- точно большие числа 65 535 и 65 536 вызовут переполнение разрядной сетки.
В программе следует для такого случая использовать результат c типом long
long и прямо указать, что множитель имеет тот же тип (long long): 65535· 65536 будет вычислено правильно. В Visual Studio тип __int64 это аналог типа long
long, который соответствует 64-битному целому числу.
3.2. Переменные
Переменная – это буквенно-цифровое обозначение операнда, то есть ячейки памяти, из которой будут читаться и в которую могут писаться значе- ния и результаты вычислений. Допускаются буквы латиницы, цифры и сим- волы подчеркивания, причем начинаться имя переменной должно с буквы. За- главные и строчные буквы считаются разными в именах переменных:
int A;//объявление целой со знаком переменной A в программе, ее значение
пока не определено.
3.3. Арифметические операции
В качестве основных арифметических операций можно выделить следу- ющие:
− присвоение «A = 2» (слева результат, справа значение, которое будет записано в A (2), это уже определение значения переменной);
− сложение «+»;
− вычитание «–»;
− умножение «*»;
− деление «/»;
− остаток от целочисленного деления «%».
Это операции, выполняющиеся над парой переменных или констант
(например, A·2 или 2·2), называются бинарными.
Кроме того, возможны операции с одной переменной – унарные. К таким, например, относитсяинкремент «++» (увеличение на 1).
A++ и ++A – это префиксная и постфиксная формы инкремента.
Пример:
A = 2;
A++; // A стало равно 3.
19
4. ЧИСЛА И ИХ ПРЕДСТАВЛЕНИЕ В МАШИННЫХ КОДАХ
Остановимся на представлении чисел в двоичном коде, то есть попробуем взглянуть на мир со стороны нашего вычислителя. Например, как выглядит число, соответствующее 2021 году в машинном представлении, в двоичном 32 битном коде. Двоичный код – позиционная система счисления, в которой стар- шие разряды находятся слева и весовой коэффициент каждого разряда равен степени числа «2» (основания системы счисления). Показатель степени на еди- ницу меньше номера цифры в числе, считая от младших разрядов. То есть для цифры младшего разряда это 2 0
= 1. Старшие разряды имеют коэффициенты:
2, 4, 8, 16, 32, 64….
Как можно разложить 2021 по разрядам в десятичной системе?
Например, делим число на весовой коэффициент 4-го разряда десятичной системы: 10
(4-1)
= 1000 и получаем 2. Дальше остаток от деления делим на 10 2
, получаем 0 и 21 в остатке и так далее до 10 0
, в результате получим 2021.
Применим ту же технику к числу 2021 и двоичному коду. Так как наш старший разряд 32, то начинаем делить 2021 на 2 31
– не делится, пишем 0 и в остатке 2021. Дальше, пока не дойдем до 11-го разряда, результат будет 0, и только 2 10
=1024 даст при делении 1 и 997 в остатке. Продолжив, получим код
11111100101. Выполнение таких действий – скучная процедура, в ней слиш- ком много повторяющихся действий. Хорошо бы их описать коротким тек- стом программы для нашей машины, чтобы та посчитала и напечатала двоич- ный код неотрицательного числа в консольном приложении.
Алгоритм в данном случае содержит выполнение повторяющихся дей- ствий, пока мы не пробежим по всем разрядам нашего числа от 32 до 1. Тут возникает необходимость в управляющих элементах языка Си.
4.1. Управляющие элементы языка Си
К ним относятся операторы условия if, переключатели switch и операторы цикла for, do while. Задачей элементов управления является выполнение пере- хода к следующему действию в программе в зависимости от определенных условий, которые считаются булевскими типами. Выражение проверки про- стого условия:
if ( A )
B = 2;
Это означает, что программа проверит А, приведет его к булевскому типу, и, если А не 0 (любое число отличное от 0), то выполнится следующий за усло- вием оператор – переменной B будет присвоено значение 2. То есть в ячейку
20 памяти В программа запишет число 2. Точка с запятой в С используется как символ конца операции. Если по условию выполняются несколько действий, то эти действия (тело цикла) должны быть заключены в фигурные скобки «{» и «}».
4.2. Блоки кода
Блоки в фигурных скобках группируют код и одновременно являются фрагментом кода, в котором могут определяться временные переменные. Этот блок будет областью видимости такой переменной.
Правило: объявленная в блоке переменная за пределами блока стано- вится неопределенной.
Оператор проверки нескольких вариантов значений нашей переменной
switch (A) { case 1: B=2; break; case 2: B = 3; break; } проверяет А на равенство значениям 1, а потом 2 и если выполнится одно из условий (компилятор не разрешит одинаковых проверок), то будет выполнен следующий за условием код. Здесь break; означает прекращение действий, выполняемых по условию
case.
Оператор цикла for выполняет заданное количество действий:
for ( i=0: i<32; i++ ) { /* операторы, выполняемые в цикле */}
Программа выполнит операции в фигурных скобках ровно 32 раза, где i – параметр цикла, целое число. Первый элемент цикла – задание начального зна- чения – i (равного 0). Второй – проверка выполнения условия цикла i<32 –
пока это TRUE (истинно), цикл выполняется, а при FALSE (ложно) переходим к следующему за скобками действию. Третий – изменение параметра цикла на каждом проходе – в данном случае «i++» – инкремент параметра цикла, уве- личение i на 1 при каждом проходе по циклу.
Часто допускают ошибку – ставят знак точку с запятой «;» между круг- лыми скобками и фигурными скобками:
for ( i=0: i<32; i++ ); { /* операторы, выполняемые в цикле */}
Это создает так называемый пустой цикл, точка с запятой считаются в нем оператором, который ничего не делает, он повторится 32 раза, после чего дей- ствие в скобках будет выполнено только один раз.
Для лучшего структурирования кода в языке определяется блок операций, в коде блок заключается в фигурные скобки:
{ /* переменные, которые определены в этом блоке, операторы, выпол-
няемые в блоке */}
21
Блок в свою очередь может содержать вложенные блоки. Количество вло- жений не ограничено. Вложенные блоки для лучшего восприятия кода при- нято смещать на табуляцию вправо, то есть «внутрь» текста внешнего блока:
{//начало блока верхнего уровня
int A; // переменная А определена и в этом и во вложенном блоках
…
{//начало вложенного блока
int B; // переменная В определена только в этом блоке
…
}//конец вложенного блока, переменная В освобождается и далее //не
определена
}//конец блока верхнего уровня
Блок является хранилищем операций, выполняемых во фрагменте кода, называемом функцией. Функция, как правило, пишется для тех одинаковых действий, которые выполняются в различных частях программы.
4.3. Функции и блоки
Функции в языке С играют очень важную роль. Функция имеет имя и спи- сок параметров (аргументов), передаваемых в функцию, который заключается в круглых скобках:
main(int arg)
{ /*блок: переменные и операторы, выполняемые в программе */
} //пример функции.
Приведенная выше функция является обязательной. Это главная функция кода в консольном приложении, с которой начинается выполнение вашей про- граммы. Эту функцию для нас помощник создает автоматически при создании проекта консольного приложения.
Ввод и вывод в консольных приложениях в Си выполняется функциями
scanf(, address) и printf(/ value) . Эти функции являются ча- стью стандарта языка Си и осуществляют много операций – чтение кодов нажатых клавиш при вводе, генерацию символов шрифта при выводе, осво- бождая вас от такой необходимости. Ввод и вывод в Си работает с перемен- ными, а переменные имеют строго определенный тип, поэтому аргументы функций сообщают какой именно тип будет считываться с клавиатуры или вы- водиться на экран. Этот спецификатор типа называется форматом – вот от- куда взялась буква f в конце имени функций. Функция ввода принимает
22 формат в качестве аргумента – это текстовая константа. Например, ввод и вы- вод целого десятичного числа имеет формат «%d».
Также при вводе мы должны передать в функцию адрес переменной:
scanf( “%d”, &A); //считывает с клавиатуры символы, пытается пере-
вести их в десятичное число и записать результат в переменную А. Адрес пе-
ременной в языке Си – оператор «&». &A – получить адрес переменной.
printf( “%d”, A);//тут проще: взять значение из переменной А и напеча-
тать его в текущую позицию курсора на экране консоли.
Для Visual Studio безопасным будет использование функций scanf_s и
printf_s, в противном случае компилятор выдаст ошибку/предупреждение.
4.4. Задание на лабораторную работу № 1
Написать консольное приложение на языке Си, которое переводит деся- тичное число, введенное оператором, в 32 битный двоичный код с использо- ванием функций ввода scanf, цикла for, оператора условия if и функции вывода
printf.
23
5. ОТРИЦАТЕЛЬНЫЕ ЧИСЛА В ДВОИЧНОМ КОДЕ
Основной тип int – число со знаком, и мы уже знаем, что в самом старшем бите кода находится отведенный под знак числа разряд (знаковый разряд), ко- торый принимает значение 1 для отрицательных чисел. Разберемся, откуда происходит подобное представление.
5.1. Шестнадцатеричный код
Пусть дано целое положительное число 1, для сокращения записи огра- ничимся типом char, тогда двоичный код числа: 0000 0001. Этот код записан двумя порциями четырехразрядных чисел – тетрадами. Это удобно для пред- ставления шестнадцатеричного кода, в котором каждая цифра или буква соот- ветствует тетраде, так что запись укорачивается до 0x01 – это то же самое число 1. Каждым разрядом шестнадцатеричного числа может быть цифра от 0 до 9 или буква от A до F (заглавная или прописная). Таким образом кодиру- ются все 16 комбинаций в тетраде.
В качестве -1 подберем такое двоичное число, чтобы выполнялось ариф- метическое тождество 1 + (-1) = 0. При выполнении суммирования двоичных чисел разряды складываются с переносом:
0000 0001
+
1111 1111
=
0000 0000 //перенос в 9 разряд, но его нет, так что единица «теряется»
Из этого следует, что число -1 выглядит как 1111 1111 в двоичном коде, или 0xFF в шестнадцатеричном представлении. Здесь обе тетрады имеют дво- ичный код 1111, это десятичное число 15, которому соответствует буква F в шестнадцатеричном коде.
Мы видим, что в числе, соответствующем -1 действительно 1 в старшем разряде. Тождество -1 + 1 = 0 именно так и выполняется двоичным суммато- ром, который есть в каждом процессоре каждого вычислителя.
Теперь обобщим наш результат для получения правила получения двоич- ного кода целого отрицательного числа. Чтобы получить нуль в сумме с таким же, но положительным числом, нужно все разряды положительного числа ин- вертировать (поменять 0 на 1, а 1 на 0 в каждом разряде). Однако, в этом случае сумма будет числом, во всех разрядах которого будут единицы. После этого к этому числу нужно прибавить еще 1, тогда в сумме получим 0 во всех
24 разрядах. Возникнет перенос в старшем разряде за пределы разрядной сетки, который не считается. Имеем тождество:
A + A + 1 = 0, где А – положительное число (и его старший разряд всегда 0), A – обратный код числа А (его поразрядная инверсия). Данное тождество можно переписать в виде:
A + (A + 1) = 0, или
A – A = 0, значит
-А = A + 1.
Такое представление отрицательных чисел называют дополнительным
кодом, поскольку сперва из кода положительного числа получают обратный двоичный код, затем к нему прибавляют единицу, то есть дополняют единицей инверсию исходного кода. Интересно, как это сработает с числом 0, исходный код которого – 0000 0000, следовательно, обратный двоичный код (инверсия)
– 1111 1111. В соответствии с полученным правилом, прибавляем к нему 1.
Получаем 0000 0000 – то есть нуль взятый со знаком «минус» в двоичной арифметике остался тем же нулем – правильный результат.
Вернемся к нашему методу представления чисел в двоичном коде путем поразрядного деления числа на весовой коэффициент текущего двоичного раз- ряда. По сути, этим действием мы определяли простую альтернативу: нуль или единицу нужно напечатать в данном разряде двоичного числа. Двигаясь от старшего 32-го разряда к младшему первому, мы таким образом заполняли ну- лями и единицами 32 разряда двоичного числа, которое и являлось ответом в задаче. Для отрицательных чисел такой алгоритм без модификаций не рабо- тает – деление будет давать отрицательное число, и результат будет непра- вильным.
Попробуем найти ответ на вопрос «нуль или единица стоит в данном раз- ряде двоичного числа» другим способом. Используем наши знания о двоичной операции «И», которая дает в результате 1 только когда оба операнда равны 1.
Это работает как для булевых переменных, так и для каждого из разрядов дво- ичного числа. Различие состоит в том, что операция «&&» сперва осуществит приведение чисел – операндов к булевскому типу (все числа отличные от нуля
– TRUE, число 0 – FALSE). Операция «&» выполнит поразрядное логическое
«И» (конъюнкция), то есть в каждом разряде первого и второго чисел будет
25 выполнено «И» только над этими значениями в данном разряде, и в этот же разряд будет записан результат операции «И».
Такое действие, относится к логическим операциям и выполняется одно- временно со всеми 32 разрядами нашего числа в арифметико-логическом устройстве (АЛУ) нашего вычислителя.
Можем использовать эту операцию для ответа на вопрос: единица или нуль стоит в проверяемом разряде нашего двоичного числа. Для этого сфор- мируем так называемую «маску» – специальное число, только в одном разряде которого (в проверяемом на данном шаге) выставляется 1, а во всех остальных разрядах – 0. Для 32-го разряда числа это 1000 0000 0000 0000 0000 0000 0000 0000 – двоичное число, которое в шестнадцатеричном коде будет гораздо ком- пактнее – 0х80000000. Заметим, что старший разряд этого числа – 1, то есть число с типом int отрицательное. Для битовой маски есть смысл использовать беззнаковый тип unsigned int или DWORD, который исключает влияние знака на результат.
Тип DWORD так называется потому, что состоит из двух 16-разрядных слов, а каждое 16-разрядное слово – из двух байт. Когда вычислители были 16- разрядными, типы «байт» (byte) и «слово» (word) служили для различения 8- разрядных и 16-разрядных чисел-операндов. Для 32-разрядного вычислителя
double word сократился в DWORD. Ну и в 64-разрядных вычислителях числа становятся QWORD (Quadro word) – четыре шестнадцатеричных слова, каж- дое по 2 байта – всего восемь байт в таком целом числе. Напомним, что в Cи тип кодируется как long long.
Пока для 32-разрядной системы используем переменную типа
unsigned int Maska = 0х80000000; // начальное значение маски
Выполняем операцию проверки старшего бита числа (и совершенно не- важно, положительное оно или отрицательное, арифметики в операции про- верки теперь нет, мы заменили ее логической операцией, в которой никакого влияния на результат знак числа не оказывает, проверяются только биты):
if (A & Maska ) //поразрядное логическое «И», при этом само число A не
//меняется
printf(“1”); //печать 1 на консоли
else
printf(“0”); //печать 0 на консоли
Старший разряд мы проверили. Теперь хотим проверить 31-й разряд числа. Для этого маска должна стать 0100 0000 0000 0000 0000 0000 0000 0000.
26
Это соответствует результату деления Maska на 2, то есть можно написать унарную операцию:
Maska /= 2;
Но по виду маски можно просто сдвинуть единицу в ней вправо на 1 раз- ряд. Оказывается, операция деления двоичного числа на 2 – это просто сдвиг всех его битов вправо на один разряд. В АЛУ эта операция называется ариф-
метический сдвиг вправо, и символ ее в языке Си два символа «>» подряд –
«>>»:
Maska = Maska >> 1;//сдвинуть содержимое числа Maska вправо на 1 раз-
ряд, результат записать в эту же переменную Maska.
Короткая унарная запись того же действия:
Maska >>= 1;
В языке С есть и операция сдвига разрядов числа влево, ее синтаксическое обозначение: «<<». Например, сдвиг влево на 3 разряда можно записать так:
A <<= 3;
Логические операции проверки битов чисел в вычислителях часто назы- вают проверкой «флагов». Аналогия заключается в следующем: каждый бит числа может отвечать за какой-то отдельный признак, например первый бит – включен мотор; второй бит – направление вращения мотора. Значение бита 1 похоже на символическое обозначение флага на флагштоке если нет ветра
(флаг – хвостик у единицы, а флагшток – вертикальная линия). Процедуру про- верки произвольного бита числа нужно оформить функцией.
32>
1 2 3 4 5 6
необходимо учитывать
какого типа и размера может получиться результат вычислений. Так, например значение переменной «v» согласно логике работы программы полу- чилось равным 2548, а не 4.
17
3. ЭЛЕМЕНТЫ ЯЗЫКА Си
Наше восприятие текста (а числа у нас – часть текста) не делает разницы между символом и его значением. Совсем другое дело – вычислительная си- стема. Как мы выяснили там циркулирует только двоичные коды, а поскольку в наших программах встречаются различные данные, в языке Си обязательно определяются типы данных при объявлении переменных. Например:
int a,b;//объявляет целочисленные переменные а и b.
Здесь:
− разделитель, точка с запятой «;»;
− перечисление, запятая «,»;
− комментарии: «/* комментарий */» или «// комментарий до конца строки».
3.1. Типы данных
Для логических операций используется булевский тип данных BOOL, ко- торый в Си представлен целым числом и имеет два значения TRUE (1) и FALSE
(0).
Целый тип данных int – это число со знаком, которое в нашей (x86) си- стеме имеет 32 двоичных разряда. Диапазон от −2 147 483 648 до
+2 147 483 647. Из них один старший разряд играет роль знака (ведь в двоич- ном коде нет «минуса»): если значение старшего разряда 1 – это «минус», число отрицательное, если 0 – неотрицательное число.
Целые могут быть: короткие short – 16 двоичных разрядов, long – 32 дво- ичных разряда (полный аналог int, long long – 64двоичных разряда) и самые короткие char – всего один байт или 8 двоичных разрядов. Аналогия char – символ, и действительно, в простейшем случае стандартной ANSI-таблицы символов одного байта достаточно для кодирования латиницы, цифр и симво- лов нашей клавиатуры.
Текстовые константы в коде: строка, “A” – в двойных кавычках, либо один символ из строки, ‘A’ – в одиночных кавычках
Для работы с перечисляемыми данными достаточно натуральных чисел.
Тогда отпадает необходимость в использовании старшего разряда числа под знак, а диапазон значений становится от 0 до 4 294 967 295 (а комбинаций ну- лей и единиц столько же). Соответствующий тип unsigned int – беззнаковое целое, аналогично unsigned short, unsigned long long и unsigned char.
Правило: без unsigned все целые типы считаются числами со знаком.
18
Серьезный вопрос: что делает наша программа, когда выполняет действия с используемыми типами данных, например, при перемножении двух целых чисел типа int выполняется приведение результата к типу int. При этом доста- точно большие числа 65 535 и 65 536 вызовут переполнение разрядной сетки.
В программе следует для такого случая использовать результат c типом long
long и прямо указать, что множитель имеет тот же тип (long long): 65535· 65536 будет вычислено правильно. В Visual Studio тип __int64 это аналог типа long
long, который соответствует 64-битному целому числу.
3.2. Переменные
Переменная – это буквенно-цифровое обозначение операнда, то есть ячейки памяти, из которой будут читаться и в которую могут писаться значе- ния и результаты вычислений. Допускаются буквы латиницы, цифры и сим- волы подчеркивания, причем начинаться имя переменной должно с буквы. За- главные и строчные буквы считаются разными в именах переменных:
int A;//объявление целой со знаком переменной A в программе, ее значение
пока не определено.
3.3. Арифметические операции
В качестве основных арифметических операций можно выделить следу- ющие:
− присвоение «A = 2» (слева результат, справа значение, которое будет записано в A (2), это уже определение значения переменной);
− сложение «+»;
− вычитание «–»;
− умножение «*»;
− деление «/»;
− остаток от целочисленного деления «%».
Это операции, выполняющиеся над парой переменных или констант
(например, A·2 или 2·2), называются бинарными.
Кроме того, возможны операции с одной переменной – унарные. К таким, например, относитсяинкремент «++» (увеличение на 1).
A++ и ++A – это префиксная и постфиксная формы инкремента.
Пример:
A = 2;
A++; // A стало равно 3.
19
4. ЧИСЛА И ИХ ПРЕДСТАВЛЕНИЕ В МАШИННЫХ КОДАХ
Остановимся на представлении чисел в двоичном коде, то есть попробуем взглянуть на мир со стороны нашего вычислителя. Например, как выглядит число, соответствующее 2021 году в машинном представлении, в двоичном 32 битном коде. Двоичный код – позиционная система счисления, в которой стар- шие разряды находятся слева и весовой коэффициент каждого разряда равен степени числа «2» (основания системы счисления). Показатель степени на еди- ницу меньше номера цифры в числе, считая от младших разрядов. То есть для цифры младшего разряда это 2 0
= 1. Старшие разряды имеют коэффициенты:
2, 4, 8, 16, 32, 64….
Как можно разложить 2021 по разрядам в десятичной системе?
Например, делим число на весовой коэффициент 4-го разряда десятичной системы: 10
(4-1)
= 1000 и получаем 2. Дальше остаток от деления делим на 10 2
, получаем 0 и 21 в остатке и так далее до 10 0
, в результате получим 2021.
Применим ту же технику к числу 2021 и двоичному коду. Так как наш старший разряд 32, то начинаем делить 2021 на 2 31
– не делится, пишем 0 и в остатке 2021. Дальше, пока не дойдем до 11-го разряда, результат будет 0, и только 2 10
=1024 даст при делении 1 и 997 в остатке. Продолжив, получим код
11111100101. Выполнение таких действий – скучная процедура, в ней слиш- ком много повторяющихся действий. Хорошо бы их описать коротким тек- стом программы для нашей машины, чтобы та посчитала и напечатала двоич- ный код неотрицательного числа в консольном приложении.
Алгоритм в данном случае содержит выполнение повторяющихся дей- ствий, пока мы не пробежим по всем разрядам нашего числа от 32 до 1. Тут возникает необходимость в управляющих элементах языка Си.
4.1. Управляющие элементы языка Си
К ним относятся операторы условия if, переключатели switch и операторы цикла for, do while. Задачей элементов управления является выполнение пере- хода к следующему действию в программе в зависимости от определенных условий, которые считаются булевскими типами. Выражение проверки про- стого условия:
if ( A )
B = 2;
Это означает, что программа проверит А, приведет его к булевскому типу, и, если А не 0 (любое число отличное от 0), то выполнится следующий за усло- вием оператор – переменной B будет присвоено значение 2. То есть в ячейку
20 памяти В программа запишет число 2. Точка с запятой в С используется как символ конца операции. Если по условию выполняются несколько действий, то эти действия (тело цикла) должны быть заключены в фигурные скобки «{» и «}».
4.2. Блоки кода
Блоки в фигурных скобках группируют код и одновременно являются фрагментом кода, в котором могут определяться временные переменные. Этот блок будет областью видимости такой переменной.
Правило: объявленная в блоке переменная за пределами блока стано- вится неопределенной.
Оператор проверки нескольких вариантов значений нашей переменной
switch (A) { case 1: B=2; break; case 2: B = 3; break; } проверяет А на равенство значениям 1, а потом 2 и если выполнится одно из условий (компилятор не разрешит одинаковых проверок), то будет выполнен следующий за условием код. Здесь break; означает прекращение действий, выполняемых по условию
case.
Оператор цикла for выполняет заданное количество действий:
for ( i=0: i<32; i++ ) { /* операторы, выполняемые в цикле */}
Программа выполнит операции в фигурных скобках ровно 32 раза, где i – параметр цикла, целое число. Первый элемент цикла – задание начального зна- чения – i (равного 0). Второй – проверка выполнения условия цикла i<32 –
пока это TRUE (истинно), цикл выполняется, а при FALSE (ложно) переходим к следующему за скобками действию. Третий – изменение параметра цикла на каждом проходе – в данном случае «i++» – инкремент параметра цикла, уве- личение i на 1 при каждом проходе по циклу.
Часто допускают ошибку – ставят знак точку с запятой «;» между круг- лыми скобками и фигурными скобками:
for ( i=0: i<32; i++ ); { /* операторы, выполняемые в цикле */}
Это создает так называемый пустой цикл, точка с запятой считаются в нем оператором, который ничего не делает, он повторится 32 раза, после чего дей- ствие в скобках будет выполнено только один раз.
Для лучшего структурирования кода в языке определяется блок операций, в коде блок заключается в фигурные скобки:
{ /* переменные, которые определены в этом блоке, операторы, выпол-
няемые в блоке */}
21
Блок в свою очередь может содержать вложенные блоки. Количество вло- жений не ограничено. Вложенные блоки для лучшего восприятия кода при- нято смещать на табуляцию вправо, то есть «внутрь» текста внешнего блока:
{//начало блока верхнего уровня
int A; // переменная А определена и в этом и во вложенном блоках
…
{//начало вложенного блока
int B; // переменная В определена только в этом блоке
…
}//конец вложенного блока, переменная В освобождается и далее //не
определена
}//конец блока верхнего уровня
Блок является хранилищем операций, выполняемых во фрагменте кода, называемом функцией. Функция, как правило, пишется для тех одинаковых действий, которые выполняются в различных частях программы.
4.3. Функции и блоки
Функции в языке С играют очень важную роль. Функция имеет имя и спи- сок параметров (аргументов), передаваемых в функцию, который заключается в круглых скобках:
main(int arg)
{ /*блок: переменные и операторы, выполняемые в программе */
} //пример функции.
Приведенная выше функция является обязательной. Это главная функция кода в консольном приложении, с которой начинается выполнение вашей про- граммы. Эту функцию для нас помощник создает автоматически при создании проекта консольного приложения.
Ввод и вывод в консольных приложениях в Си выполняется функциями
scanf(, address) и printf(/ value) . Эти функции являются ча- стью стандарта языка Си и осуществляют много операций – чтение кодов нажатых клавиш при вводе, генерацию символов шрифта при выводе, осво- бождая вас от такой необходимости. Ввод и вывод в Си работает с перемен- ными, а переменные имеют строго определенный тип, поэтому аргументы функций сообщают какой именно тип будет считываться с клавиатуры или вы- водиться на экран. Этот спецификатор типа называется форматом – вот от- куда взялась буква f в конце имени функций. Функция ввода принимает
22 формат в качестве аргумента – это текстовая константа. Например, ввод и вы- вод целого десятичного числа имеет формат «%d».
Также при вводе мы должны передать в функцию адрес переменной:
scanf( “%d”, &A); //считывает с клавиатуры символы, пытается пере-
вести их в десятичное число и записать результат в переменную А. Адрес пе-
ременной в языке Си – оператор «&». &A – получить адрес переменной.
printf( “%d”, A);//тут проще: взять значение из переменной А и напеча-
тать его в текущую позицию курсора на экране консоли.
Для Visual Studio безопасным будет использование функций scanf_s и
printf_s, в противном случае компилятор выдаст ошибку/предупреждение.
4.4. Задание на лабораторную работу № 1
Написать консольное приложение на языке Си, которое переводит деся- тичное число, введенное оператором, в 32 битный двоичный код с использо- ванием функций ввода scanf, цикла for, оператора условия if и функции вывода
printf.
23
5. ОТРИЦАТЕЛЬНЫЕ ЧИСЛА В ДВОИЧНОМ КОДЕ
Основной тип int – число со знаком, и мы уже знаем, что в самом старшем бите кода находится отведенный под знак числа разряд (знаковый разряд), ко- торый принимает значение 1 для отрицательных чисел. Разберемся, откуда происходит подобное представление.
5.1. Шестнадцатеричный код
Пусть дано целое положительное число 1, для сокращения записи огра- ничимся типом char, тогда двоичный код числа: 0000 0001. Этот код записан двумя порциями четырехразрядных чисел – тетрадами. Это удобно для пред- ставления шестнадцатеричного кода, в котором каждая цифра или буква соот- ветствует тетраде, так что запись укорачивается до 0x01 – это то же самое число 1. Каждым разрядом шестнадцатеричного числа может быть цифра от 0 до 9 или буква от A до F (заглавная или прописная). Таким образом кодиру- ются все 16 комбинаций в тетраде.
В качестве -1 подберем такое двоичное число, чтобы выполнялось ариф- метическое тождество 1 + (-1) = 0. При выполнении суммирования двоичных чисел разряды складываются с переносом:
0000 0001
+
1111 1111
=
0000 0000 //перенос в 9 разряд, но его нет, так что единица «теряется»
Из этого следует, что число -1 выглядит как 1111 1111 в двоичном коде, или 0xFF в шестнадцатеричном представлении. Здесь обе тетрады имеют дво- ичный код 1111, это десятичное число 15, которому соответствует буква F в шестнадцатеричном коде.
Мы видим, что в числе, соответствующем -1 действительно 1 в старшем разряде. Тождество -1 + 1 = 0 именно так и выполняется двоичным суммато- ром, который есть в каждом процессоре каждого вычислителя.
Теперь обобщим наш результат для получения правила получения двоич- ного кода целого отрицательного числа. Чтобы получить нуль в сумме с таким же, но положительным числом, нужно все разряды положительного числа ин- вертировать (поменять 0 на 1, а 1 на 0 в каждом разряде). Однако, в этом случае сумма будет числом, во всех разрядах которого будут единицы. После этого к этому числу нужно прибавить еще 1, тогда в сумме получим 0 во всех
24 разрядах. Возникнет перенос в старшем разряде за пределы разрядной сетки, который не считается. Имеем тождество:
A + A + 1 = 0, где А – положительное число (и его старший разряд всегда 0), A – обратный код числа А (его поразрядная инверсия). Данное тождество можно переписать в виде:
A + (A + 1) = 0, или
A – A = 0, значит
-А = A + 1.
Такое представление отрицательных чисел называют дополнительным
кодом, поскольку сперва из кода положительного числа получают обратный двоичный код, затем к нему прибавляют единицу, то есть дополняют единицей инверсию исходного кода. Интересно, как это сработает с числом 0, исходный код которого – 0000 0000, следовательно, обратный двоичный код (инверсия)
– 1111 1111. В соответствии с полученным правилом, прибавляем к нему 1.
Получаем 0000 0000 – то есть нуль взятый со знаком «минус» в двоичной арифметике остался тем же нулем – правильный результат.
Вернемся к нашему методу представления чисел в двоичном коде путем поразрядного деления числа на весовой коэффициент текущего двоичного раз- ряда. По сути, этим действием мы определяли простую альтернативу: нуль или единицу нужно напечатать в данном разряде двоичного числа. Двигаясь от старшего 32-го разряда к младшему первому, мы таким образом заполняли ну- лями и единицами 32 разряда двоичного числа, которое и являлось ответом в задаче. Для отрицательных чисел такой алгоритм без модификаций не рабо- тает – деление будет давать отрицательное число, и результат будет непра- вильным.
Попробуем найти ответ на вопрос «нуль или единица стоит в данном раз- ряде двоичного числа» другим способом. Используем наши знания о двоичной операции «И», которая дает в результате 1 только когда оба операнда равны 1.
Это работает как для булевых переменных, так и для каждого из разрядов дво- ичного числа. Различие состоит в том, что операция «&&» сперва осуществит приведение чисел – операндов к булевскому типу (все числа отличные от нуля
– TRUE, число 0 – FALSE). Операция «&» выполнит поразрядное логическое
«И» (конъюнкция), то есть в каждом разряде первого и второго чисел будет
25 выполнено «И» только над этими значениями в данном разряде, и в этот же разряд будет записан результат операции «И».
Такое действие, относится к логическим операциям и выполняется одно- временно со всеми 32 разрядами нашего числа в арифметико-логическом устройстве (АЛУ) нашего вычислителя.
Можем использовать эту операцию для ответа на вопрос: единица или нуль стоит в проверяемом разряде нашего двоичного числа. Для этого сфор- мируем так называемую «маску» – специальное число, только в одном разряде которого (в проверяемом на данном шаге) выставляется 1, а во всех остальных разрядах – 0. Для 32-го разряда числа это 1000 0000 0000 0000 0000 0000 0000 0000 – двоичное число, которое в шестнадцатеричном коде будет гораздо ком- пактнее – 0х80000000. Заметим, что старший разряд этого числа – 1, то есть число с типом int отрицательное. Для битовой маски есть смысл использовать беззнаковый тип unsigned int или DWORD, который исключает влияние знака на результат.
Тип DWORD так называется потому, что состоит из двух 16-разрядных слов, а каждое 16-разрядное слово – из двух байт. Когда вычислители были 16- разрядными, типы «байт» (byte) и «слово» (word) служили для различения 8- разрядных и 16-разрядных чисел-операндов. Для 32-разрядного вычислителя
double word сократился в DWORD. Ну и в 64-разрядных вычислителях числа становятся QWORD (Quadro word) – четыре шестнадцатеричных слова, каж- дое по 2 байта – всего восемь байт в таком целом числе. Напомним, что в Cи тип кодируется как long long.
Пока для 32-разрядной системы используем переменную типа
unsigned int Maska = 0х80000000; // начальное значение маски
Выполняем операцию проверки старшего бита числа (и совершенно не- важно, положительное оно или отрицательное, арифметики в операции про- верки теперь нет, мы заменили ее логической операцией, в которой никакого влияния на результат знак числа не оказывает, проверяются только биты):
if (A & Maska ) //поразрядное логическое «И», при этом само число A не
//меняется
printf(“1”); //печать 1 на консоли
else
printf(“0”); //печать 0 на консоли
Старший разряд мы проверили. Теперь хотим проверить 31-й разряд числа. Для этого маска должна стать 0100 0000 0000 0000 0000 0000 0000 0000.
26
Это соответствует результату деления Maska на 2, то есть можно написать унарную операцию:
Maska /= 2;
Но по виду маски можно просто сдвинуть единицу в ней вправо на 1 раз- ряд. Оказывается, операция деления двоичного числа на 2 – это просто сдвиг всех его битов вправо на один разряд. В АЛУ эта операция называется ариф-
метический сдвиг вправо, и символ ее в языке Си два символа «>» подряд –
«>>»:
Maska = Maska >> 1;//сдвинуть содержимое числа Maska вправо на 1 раз-
ряд, результат записать в эту же переменную Maska.
Короткая унарная запись того же действия:
Maska >>= 1;
В языке С есть и операция сдвига разрядов числа влево, ее синтаксическое обозначение: «<<». Например, сдвиг влево на 3 разряда можно записать так:
A <<= 3;
Логические операции проверки битов чисел в вычислителях часто назы- вают проверкой «флагов». Аналогия заключается в следующем: каждый бит числа может отвечать за какой-то отдельный признак, например первый бит – включен мотор; второй бит – направление вращения мотора. Значение бита 1 похоже на символическое обозначение флага на флагштоке если нет ветра
(флаг – хвостик у единицы, а флагшток – вертикальная линия). Процедуру про- верки произвольного бита числа нужно оформить функцией.
32>
1 2 3 4 5 6
какого типа и размера может получиться результат вычислений. Так, например значение переменной «v» согласно логике работы программы полу- чилось равным 2548, а не 4.
17
3. ЭЛЕМЕНТЫ ЯЗЫКА Си
Наше восприятие текста (а числа у нас – часть текста) не делает разницы между символом и его значением. Совсем другое дело – вычислительная си- стема. Как мы выяснили там циркулирует только двоичные коды, а поскольку в наших программах встречаются различные данные, в языке Си обязательно определяются типы данных при объявлении переменных. Например:
int a,b;//объявляет целочисленные переменные а и b.
Здесь:
− разделитель, точка с запятой «;»;
− перечисление, запятая «,»;
− комментарии: «/* комментарий */» или «// комментарий до конца строки».
3.1. Типы данных
Для логических операций используется булевский тип данных BOOL, ко- торый в Си представлен целым числом и имеет два значения TRUE (1) и FALSE
(0).
Целый тип данных int – это число со знаком, которое в нашей (x86) си- стеме имеет 32 двоичных разряда. Диапазон от −2 147 483 648 до
+2 147 483 647. Из них один старший разряд играет роль знака (ведь в двоич- ном коде нет «минуса»): если значение старшего разряда 1 – это «минус», число отрицательное, если 0 – неотрицательное число.
Целые могут быть: короткие short – 16 двоичных разрядов, long – 32 дво- ичных разряда (полный аналог int, long long – 64двоичных разряда) и самые короткие char – всего один байт или 8 двоичных разрядов. Аналогия char – символ, и действительно, в простейшем случае стандартной ANSI-таблицы символов одного байта достаточно для кодирования латиницы, цифр и симво- лов нашей клавиатуры.
Текстовые константы в коде: строка, “A” – в двойных кавычках, либо один символ из строки, ‘A’ – в одиночных кавычках
Для работы с перечисляемыми данными достаточно натуральных чисел.
Тогда отпадает необходимость в использовании старшего разряда числа под знак, а диапазон значений становится от 0 до 4 294 967 295 (а комбинаций ну- лей и единиц столько же). Соответствующий тип unsigned int – беззнаковое целое, аналогично unsigned short, unsigned long long и unsigned char.
Правило: без unsigned все целые типы считаются числами со знаком.
18
Серьезный вопрос: что делает наша программа, когда выполняет действия с используемыми типами данных, например, при перемножении двух целых чисел типа int выполняется приведение результата к типу int. При этом доста- точно большие числа 65 535 и 65 536 вызовут переполнение разрядной сетки.
В программе следует для такого случая использовать результат c типом long
long и прямо указать, что множитель имеет тот же тип (long long): 65535· 65536 будет вычислено правильно. В Visual Studio тип __int64 это аналог типа long
long, который соответствует 64-битному целому числу.
3.2. Переменные
Переменная – это буквенно-цифровое обозначение операнда, то есть ячейки памяти, из которой будут читаться и в которую могут писаться значе- ния и результаты вычислений. Допускаются буквы латиницы, цифры и сим- волы подчеркивания, причем начинаться имя переменной должно с буквы. За- главные и строчные буквы считаются разными в именах переменных:
int A;//объявление целой со знаком переменной A в программе, ее значение
пока не определено.
3.3. Арифметические операции
В качестве основных арифметических операций можно выделить следу- ющие:
− присвоение «A = 2» (слева результат, справа значение, которое будет записано в A (2), это уже определение значения переменной);
− сложение «+»;
− вычитание «–»;
− умножение «*»;
− деление «/»;
− остаток от целочисленного деления «%».
Это операции, выполняющиеся над парой переменных или констант
(например, A·2 или 2·2), называются бинарными.
Кроме того, возможны операции с одной переменной – унарные. К таким, например, относитсяинкремент «++» (увеличение на 1).
A++ и ++A – это префиксная и постфиксная формы инкремента.
Пример:
A = 2;
A++; // A стало равно 3.
19
4. ЧИСЛА И ИХ ПРЕДСТАВЛЕНИЕ В МАШИННЫХ КОДАХ
Остановимся на представлении чисел в двоичном коде, то есть попробуем взглянуть на мир со стороны нашего вычислителя. Например, как выглядит число, соответствующее 2021 году в машинном представлении, в двоичном 32 битном коде. Двоичный код – позиционная система счисления, в которой стар- шие разряды находятся слева и весовой коэффициент каждого разряда равен степени числа «2» (основания системы счисления). Показатель степени на еди- ницу меньше номера цифры в числе, считая от младших разрядов. То есть для цифры младшего разряда это 2 0
= 1. Старшие разряды имеют коэффициенты:
2, 4, 8, 16, 32, 64….
Как можно разложить 2021 по разрядам в десятичной системе?
Например, делим число на весовой коэффициент 4-го разряда десятичной системы: 10
(4-1)
= 1000 и получаем 2. Дальше остаток от деления делим на 10 2
, получаем 0 и 21 в остатке и так далее до 10 0
, в результате получим 2021.
Применим ту же технику к числу 2021 и двоичному коду. Так как наш старший разряд 32, то начинаем делить 2021 на 2 31
– не делится, пишем 0 и в остатке 2021. Дальше, пока не дойдем до 11-го разряда, результат будет 0, и только 2 10
=1024 даст при делении 1 и 997 в остатке. Продолжив, получим код
11111100101. Выполнение таких действий – скучная процедура, в ней слиш- ком много повторяющихся действий. Хорошо бы их описать коротким тек- стом программы для нашей машины, чтобы та посчитала и напечатала двоич- ный код неотрицательного числа в консольном приложении.
Алгоритм в данном случае содержит выполнение повторяющихся дей- ствий, пока мы не пробежим по всем разрядам нашего числа от 32 до 1. Тут возникает необходимость в управляющих элементах языка Си.
4.1. Управляющие элементы языка Си
К ним относятся операторы условия if, переключатели switch и операторы цикла for, do while. Задачей элементов управления является выполнение пере- хода к следующему действию в программе в зависимости от определенных условий, которые считаются булевскими типами. Выражение проверки про- стого условия:
if ( A )
B = 2;
Это означает, что программа проверит А, приведет его к булевскому типу, и, если А не 0 (любое число отличное от 0), то выполнится следующий за усло- вием оператор – переменной B будет присвоено значение 2. То есть в ячейку
20 памяти В программа запишет число 2. Точка с запятой в С используется как символ конца операции. Если по условию выполняются несколько действий, то эти действия (тело цикла) должны быть заключены в фигурные скобки «{» и «}».
4.2. Блоки кода
Блоки в фигурных скобках группируют код и одновременно являются фрагментом кода, в котором могут определяться временные переменные. Этот блок будет областью видимости такой переменной.
Правило: объявленная в блоке переменная за пределами блока стано- вится неопределенной.
Оператор проверки нескольких вариантов значений нашей переменной
switch (A) { case 1: B=2; break; case 2: B = 3; break; } проверяет А на равенство значениям 1, а потом 2 и если выполнится одно из условий (компилятор не разрешит одинаковых проверок), то будет выполнен следующий за условием код. Здесь break; означает прекращение действий, выполняемых по условию
case.
Оператор цикла for выполняет заданное количество действий:
for ( i=0: i<32; i++ ) { /* операторы, выполняемые в цикле */}
Программа выполнит операции в фигурных скобках ровно 32 раза, где i – параметр цикла, целое число. Первый элемент цикла – задание начального зна- чения – i (равного 0). Второй – проверка выполнения условия цикла i<32 –
пока это TRUE (истинно), цикл выполняется, а при FALSE (ложно) переходим к следующему за скобками действию. Третий – изменение параметра цикла на каждом проходе – в данном случае «i++» – инкремент параметра цикла, уве- личение i на 1 при каждом проходе по циклу.
Часто допускают ошибку – ставят знак точку с запятой «;» между круг- лыми скобками и фигурными скобками:
for ( i=0: i<32; i++ ); { /* операторы, выполняемые в цикле */}
Это создает так называемый пустой цикл, точка с запятой считаются в нем оператором, который ничего не делает, он повторится 32 раза, после чего дей- ствие в скобках будет выполнено только один раз.
Для лучшего структурирования кода в языке определяется блок операций, в коде блок заключается в фигурные скобки:
{ /* переменные, которые определены в этом блоке, операторы, выпол-
няемые в блоке */}
21
Блок в свою очередь может содержать вложенные блоки. Количество вло- жений не ограничено. Вложенные блоки для лучшего восприятия кода при- нято смещать на табуляцию вправо, то есть «внутрь» текста внешнего блока:
{//начало блока верхнего уровня
int A; // переменная А определена и в этом и во вложенном блоках
…
{//начало вложенного блока
int B; // переменная В определена только в этом блоке
…
}//конец вложенного блока, переменная В освобождается и далее //не
определена
}//конец блока верхнего уровня
Блок является хранилищем операций, выполняемых во фрагменте кода, называемом функцией. Функция, как правило, пишется для тех одинаковых действий, которые выполняются в различных частях программы.
4.3. Функции и блоки
Функции в языке С играют очень важную роль. Функция имеет имя и спи- сок параметров (аргументов), передаваемых в функцию, который заключается в круглых скобках:
main(int arg)
{ /*блок: переменные и операторы, выполняемые в программе */
} //пример функции.
Приведенная выше функция является обязательной. Это главная функция кода в консольном приложении, с которой начинается выполнение вашей про- граммы. Эту функцию для нас помощник создает автоматически при создании проекта консольного приложения.
Ввод и вывод в консольных приложениях в Си выполняется функциями
scanf(
22 формат в качестве аргумента – это текстовая константа. Например, ввод и вы- вод целого десятичного числа имеет формат «%d».
Также при вводе мы должны передать в функцию адрес переменной:
scanf( “%d”, &A); //считывает с клавиатуры символы, пытается пере-
вести их в десятичное число и записать результат в переменную А. Адрес пе-
ременной в языке Си – оператор «&». &A – получить адрес переменной.
printf( “%d”, A);//тут проще: взять значение из переменной А и напеча-
тать его в текущую позицию курсора на экране консоли.
Для Visual Studio безопасным будет использование функций scanf_s и
printf_s, в противном случае компилятор выдаст ошибку/предупреждение.
4.4. Задание на лабораторную работу № 1
Написать консольное приложение на языке Си, которое переводит деся- тичное число, введенное оператором, в 32 битный двоичный код с использо- ванием функций ввода scanf, цикла for, оператора условия if и функции вывода
printf.
23
5. ОТРИЦАТЕЛЬНЫЕ ЧИСЛА В ДВОИЧНОМ КОДЕ
Основной тип int – число со знаком, и мы уже знаем, что в самом старшем бите кода находится отведенный под знак числа разряд (знаковый разряд), ко- торый принимает значение 1 для отрицательных чисел. Разберемся, откуда происходит подобное представление.
5.1. Шестнадцатеричный код
Пусть дано целое положительное число 1, для сокращения записи огра- ничимся типом char, тогда двоичный код числа: 0000 0001. Этот код записан двумя порциями четырехразрядных чисел – тетрадами. Это удобно для пред- ставления шестнадцатеричного кода, в котором каждая цифра или буква соот- ветствует тетраде, так что запись укорачивается до 0x01 – это то же самое число 1. Каждым разрядом шестнадцатеричного числа может быть цифра от 0 до 9 или буква от A до F (заглавная или прописная). Таким образом кодиру- ются все 16 комбинаций в тетраде.
В качестве -1 подберем такое двоичное число, чтобы выполнялось ариф- метическое тождество 1 + (-1) = 0. При выполнении суммирования двоичных чисел разряды складываются с переносом:
0000 0001
+
1111 1111
=
0000 0000 //перенос в 9 разряд, но его нет, так что единица «теряется»
Из этого следует, что число -1 выглядит как 1111 1111 в двоичном коде, или 0xFF в шестнадцатеричном представлении. Здесь обе тетрады имеют дво- ичный код 1111, это десятичное число 15, которому соответствует буква F в шестнадцатеричном коде.
Мы видим, что в числе, соответствующем -1 действительно 1 в старшем разряде. Тождество -1 + 1 = 0 именно так и выполняется двоичным суммато- ром, который есть в каждом процессоре каждого вычислителя.
Теперь обобщим наш результат для получения правила получения двоич- ного кода целого отрицательного числа. Чтобы получить нуль в сумме с таким же, но положительным числом, нужно все разряды положительного числа ин- вертировать (поменять 0 на 1, а 1 на 0 в каждом разряде). Однако, в этом случае сумма будет числом, во всех разрядах которого будут единицы. После этого к этому числу нужно прибавить еще 1, тогда в сумме получим 0 во всех
24 разрядах. Возникнет перенос в старшем разряде за пределы разрядной сетки, который не считается. Имеем тождество:
A + A + 1 = 0, где А – положительное число (и его старший разряд всегда 0), A – обратный код числа А (его поразрядная инверсия). Данное тождество можно переписать в виде:
A + (A + 1) = 0, или
A – A = 0, значит
-А = A + 1.
Такое представление отрицательных чисел называют дополнительным
кодом, поскольку сперва из кода положительного числа получают обратный двоичный код, затем к нему прибавляют единицу, то есть дополняют единицей инверсию исходного кода. Интересно, как это сработает с числом 0, исходный код которого – 0000 0000, следовательно, обратный двоичный код (инверсия)
– 1111 1111. В соответствии с полученным правилом, прибавляем к нему 1.
Получаем 0000 0000 – то есть нуль взятый со знаком «минус» в двоичной арифметике остался тем же нулем – правильный результат.
Вернемся к нашему методу представления чисел в двоичном коде путем поразрядного деления числа на весовой коэффициент текущего двоичного раз- ряда. По сути, этим действием мы определяли простую альтернативу: нуль или единицу нужно напечатать в данном разряде двоичного числа. Двигаясь от старшего 32-го разряда к младшему первому, мы таким образом заполняли ну- лями и единицами 32 разряда двоичного числа, которое и являлось ответом в задаче. Для отрицательных чисел такой алгоритм без модификаций не рабо- тает – деление будет давать отрицательное число, и результат будет непра- вильным.
Попробуем найти ответ на вопрос «нуль или единица стоит в данном раз- ряде двоичного числа» другим способом. Используем наши знания о двоичной операции «И», которая дает в результате 1 только когда оба операнда равны 1.
Это работает как для булевых переменных, так и для каждого из разрядов дво- ичного числа. Различие состоит в том, что операция «&&» сперва осуществит приведение чисел – операндов к булевскому типу (все числа отличные от нуля
– TRUE, число 0 – FALSE). Операция «&» выполнит поразрядное логическое
«И» (конъюнкция), то есть в каждом разряде первого и второго чисел будет
25 выполнено «И» только над этими значениями в данном разряде, и в этот же разряд будет записан результат операции «И».
Такое действие, относится к логическим операциям и выполняется одно- временно со всеми 32 разрядами нашего числа в арифметико-логическом устройстве (АЛУ) нашего вычислителя.
Можем использовать эту операцию для ответа на вопрос: единица или нуль стоит в проверяемом разряде нашего двоичного числа. Для этого сфор- мируем так называемую «маску» – специальное число, только в одном разряде которого (в проверяемом на данном шаге) выставляется 1, а во всех остальных разрядах – 0. Для 32-го разряда числа это 1000 0000 0000 0000 0000 0000 0000 0000 – двоичное число, которое в шестнадцатеричном коде будет гораздо ком- пактнее – 0х80000000. Заметим, что старший разряд этого числа – 1, то есть число с типом int отрицательное. Для битовой маски есть смысл использовать беззнаковый тип unsigned int или DWORD, который исключает влияние знака на результат.
Тип DWORD так называется потому, что состоит из двух 16-разрядных слов, а каждое 16-разрядное слово – из двух байт. Когда вычислители были 16- разрядными, типы «байт» (byte) и «слово» (word) служили для различения 8- разрядных и 16-разрядных чисел-операндов. Для 32-разрядного вычислителя
double word сократился в DWORD. Ну и в 64-разрядных вычислителях числа становятся QWORD (Quadro word) – четыре шестнадцатеричных слова, каж- дое по 2 байта – всего восемь байт в таком целом числе. Напомним, что в Cи тип кодируется как long long.
Пока для 32-разрядной системы используем переменную типа
unsigned int Maska = 0х80000000; // начальное значение маски
Выполняем операцию проверки старшего бита числа (и совершенно не- важно, положительное оно или отрицательное, арифметики в операции про- верки теперь нет, мы заменили ее логической операцией, в которой никакого влияния на результат знак числа не оказывает, проверяются только биты):
if (A & Maska ) //поразрядное логическое «И», при этом само число A не
//меняется
printf(“1”); //печать 1 на консоли
else
printf(“0”); //печать 0 на консоли
Старший разряд мы проверили. Теперь хотим проверить 31-й разряд числа. Для этого маска должна стать 0100 0000 0000 0000 0000 0000 0000 0000.
26
Это соответствует результату деления Maska на 2, то есть можно написать унарную операцию:
Maska /= 2;
Но по виду маски можно просто сдвинуть единицу в ней вправо на 1 раз- ряд. Оказывается, операция деления двоичного числа на 2 – это просто сдвиг всех его битов вправо на один разряд. В АЛУ эта операция называется ариф-
метический сдвиг вправо, и символ ее в языке Си два символа «>» подряд –
«>>»:
Maska = Maska >> 1;//сдвинуть содержимое числа Maska вправо на 1 раз-
ряд, результат записать в эту же переменную Maska.
Короткая унарная запись того же действия:
Maska >>= 1;
В языке С есть и операция сдвига разрядов числа влево, ее синтаксическое обозначение: «<<». Например, сдвиг влево на 3 разряда можно записать так:
A <<= 3;
Логические операции проверки битов чисел в вычислителях часто назы- вают проверкой «флагов». Аналогия заключается в следующем: каждый бит числа может отвечать за какой-то отдельный признак, например первый бит – включен мотор; второй бит – направление вращения мотора. Значение бита 1 похоже на символическое обозначение флага на флагштоке если нет ветра
(флаг – хвостик у единицы, а флагшток – вертикальная линия). Процедуру про- верки произвольного бита числа нужно оформить функцией.
32>
1 2 3 4 5 6
5.2. Функции и структурирование кода
Функции позволяют структурировать код и, главное, позволяют повторно использовать написанный вами код как в той же программе, так и в других ваших программах. Представьте, что у вас есть десять мест в тексте про- граммы, где нужно выполнить одно и то же действие. Можно скопировать текст уже написанного фрагмента во все эти места. Теперь представим, что потом выяснилось, что в первом фрагменте есть недочет, который нужно ис- править. При копировании фрагментов вам придется найти и подправить все десять мест в тексте, а вот если фрагмент оформлен функцией, то исправления нужно сделать только внутри функции и только один раз. Вызываться эта функция будет в вашем коде по-прежнему десять раз, но ни искать их, ни ис- правлять уже не нужно.
Функцией называется блок кода, у которого есть тип и имя. Тип функции
– это одно значение, которое функция может вернуть в вызывающий ее код:
27
int main(){ return 0;} //функция, типа int, которая возвращает значение 0
(нет ошибок) в конце блока.
Это главная функция нашей программы, и возвращается значение 0 во внешний код – в операционную систему. Очевидно, для этого нельзя исполь- зовать ячейки памяти нашей программы, ведь ее уже не будет в памяти ма- шины. Технически осуществляется хранение целого числа из 4 байтов в па- мяти временного хранения данных, в стеке. Стек организован по принципу
«последний пришел – первый вышел» (Last In First Out – LIFO). Такой прин- цип позволяет эффективно хранить переменные, как при вызове функций, так и временных, объявляющихся во вложенных блоках. Адрес таких переменных будет вычисляться относительно указателя стека.
Передача данных в функции и возврат данных из функций реализуются следующими описанными ниже способами.
5.3. Глобальные переменные
Объявляется переменная вне всех функций программы, обычно в верхней части текста (тогда по правилам эта переменная будет доступна для чтения и записи в любой точке программы, то есть становится глобальной). Способ про- стой, но имеет недостаток: место изменения значения такой переменной трудно отследить в длинном тексте. Если переменная нужна только для чте- ния, ей присваивается префикс const, и проблемы с поиском изменений пере- кладываются на компилятор, который следит за переменными с префиксом
const, изменять которые нельзя. Префикс const это аналог привычного «только для чтения» ограничения данных. Например:
const int Dim=10; //переменная для размера данных.
5.4. Параметры, передаваемые по значению
В списке аргументов функции через запятую перечисляются необходи- мые переменные с их именами и типами:
int MyFun(int A, int B); //пример функции, возвращающей целое и считы-
вающей два целых параметра.
Такой способ называют «передачей параметров по значению», поскольку можно вызвать эту функцию, подставив вместо параметров числа:
MyFun(3, 5);//правильный вызов.
Параметры int A, int B в блоке функции становятся переменными:
int MyFun(int A, int B){ return A*B;}
Идаже их значения можно менять в коде функции:
28
int MyFun(int A, int B){ A+=2; return A+B;}
Эта запись допустима, но при выходе из функции значения таких пара- метров будут потеряны, как и у всех локальных переменных в тексте функции.
5.5. Передача параметров по указателю
Параметры можно передавать в функцию по указателю. Указатель про- стой переменной – это адрес, в котором начинаются байты, занимаемые пере- менной данного типа в памяти. В Си указатель представляет собой тип дан- ных, и поскольку разные типы данных занимают разное количество байт в па- мяти, указатель определяется как «тип со звездочкой»: int * – это тип «указа- тель на целое число». Если в функцию попадает не значение переменной, а ее адрес, то, изменяя значение по этому адресу, получим возможность возвра- щать в вызывающий код произвольное количество переменных нужных типов вдобавок к возвращаемому функцией значению.
int MyFun(int * A, int * B);//функция, получающая два параметра по ука-
зателю может изменить и вернуть оба параметра и еще один через return.
Вызвать ее со значениями не получится:
MyFun(3, 5);
Это вызовет ошибку компиляции. И действительно, передав адрес «3», мы попадаем в зарезервированную область памяти и получим остановку по возникшей исключительной ситуации (исключению) – нарушению прав до- ступа c кодом 0xC00000005. В тексте функции менять A или B не будет ошиб- кой компиляции, но будет скорее всего ошибкой с нарушением памяти. Ме- нять же значения переменных, хранящихся по указателю, можно безопасно:
*A = 2;//запишет число 2 в ячейку по адресу A.
Синтаксис «*» и указатель на переменную называют разыменованием указателя, то есть при наличии звездочки перед указателем он становится пол- ным аналогом обычной переменной, той, на адрес которой он указывает. Вы- зов такой функции возможен следующим образом:
int A1, B1; MyFun( &A1, &B1);//здесь & при переменной означает, полу-
чить адрес этой переменной.
Адреса при загрузке программы в компьютер могут различаться на сосед- них машинах, так как распределением памяти для программ занимается опе- рационная система. Этот синтаксис мы видели в функции scanf (&A).
5.6. Передача параметров по ссылке (С++)
Наконец в С++ вариант передачи переменной в функцию для ввода и вы- вода данных упростили, и это называют «передача данных по ссылке».
29
Технически это то же, что и передача по указателю, а синтаксически – проще при вызове и при работе с этими переменными в теле функции.
int MyFun( int &A1, int &B1);//функция с двумя параметрами, целыми,
предаваемыми по ссылке.
Ссылка тоже тип данных и пишется тип и «&», например int & – ссылка на переменную целого типа:
int MyFun( int &A, int &B)
{ A +=2; return A*B;} //при выходе из функции первая переменная увели-
чится на 2.
5.7. Отладка функций
Первым делом убедитесь, что ваша функция выполняется в программе.
Для этого в начале исследуемой функции поставьте точку останова, см. Рис.
12. Если при запуске программы в режиме отладки произойдет остановка внутри функции, значит вызов функции в коде выполнен успешно. Правиль- ным будет проверить значения аргументов, которые переданы в функцию. Для этого подведите курсор к аргументу или найдите значение в окне отладки Lo-
cals («Локальные переменные»).
Важный вопрос: из какого места кода была вызвана функция. Для этого выберете в панели отладки закладку Call stack («Стек вызовов»). Список вы- зовов интерактивный – по щелчку можно быстро перейти к строчкам, откуда была вызвана функция.
5.8. Задание на лабораторную работу № 2
Написать консольное Win32 приложение на языке С, которое переводит десятичное число со знаком, введенное оператором, в 32-битный двоичный код с использованием функций ввода scanf_s (этобезопасный аналог scanf)
цикла for, оператора условия if и функции форматированного вывода printf.
Использовать метод проверки битов введенного числа поразрядной конъюнк- цией (логической операцией «И») числа и «маски». Маска содержит только одну единицу в исследуемом разряде двоичного числа, а остальные биты маски – нули. Проверку произвольного бита числа оформить функцией.
5.9. Пояснения к лабораторной работе № 2
Требуется проверить каждый бит, так что маска будет выглядеть как набор из 31 нуля и 1 единицы. Единица сдвигается по разрядам по мере про- верки каждого бита числа. Например, проверим первый бит числа 42:
int num = 42; //0...000101010
30
int mask = 1; //0...000000001
bool checkBit = num & mask; // checkBit = false, & – побитовый оператор
«И».
Первый (самый правый) бит числа 42 равен нулю, так что в результате операции побитового «И» получаем ноль (false). Далее:
int num = 42; //0...000101010
int mask = 1<<1; //0...00010, сдвиг единицы в маске на один разряд
bool checkBit = num & mask; //checkBit = true.
Следующий бит числа 42 равен единице, так что checkBit = true.
Используя оператор сдвига «<<», можно проверить все 32 разряда числа, ис- пользуя соответствующий цикл. По результату проверки true или false выво- дятся символы «1» или «0» соответственно. Целесообразно объединить их в одну строковую переменную. Обратите внимание, что при выполнении зада- ния нужно начинать проверку со старших разрядов, чтобы вывести биты в правильном порядке (слева направо).