Кафедра веб-технологий и компьютерного моделирования
БЕЛОРУССКИЙ ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ
МЕХАНИКО-МАТЕМАТИЧЕСКИЙ ФАКУЛЬТЕТ
Кафедра веб-технологий и компьютерного моделирования
Н. А. Аленский
МЕТОДЫ
ПРОГРАММИРОВАНИЯ:
ЛЕКЦИИ, ПРИМЕРЫ, ТЕСТЫ
ПОСОБИЕ ДЛЯ СТУДЕНТОВ
МЕХАНИКО-МАТЕМАТИЧЕСКОГО ФАКУЛЬТЕТА
В ДВУХ ЧАСТЯХ
Часть 2
МИНСК
УДК 004.432 045С++(075.8)
ББК 32.973.26-018.1я73-1
А48
Рекомендовано советом
механико-математического факультета
18 сентября 2012 г., протокол № 1
Р е ц е н з е н т
кандидат физико-математических наук,
доцент Ю. А. Кремень
Аленский, Н. А
А48 Методы программирования: лекции, примеры, тесты: пособие для студентов мех.-мат. фак. В 2 ч. Ч. 2 / Н. А. Аленский. — Минск : БГУ, 2012. — 76 с.
Используя примеры в виде отлаженных программ, упражнения и тесты, дано описание простых типов данных, указателей, одномерных и двумерных массивов и показана их связь с указателями в современном языке С++.
Адресуется, прежде всего, студентам первого курса механико-математического факультета. Пособие будет полезным также и программистам, желающим изучать язык С++. Преподаватели найдут в пособии задачи и тесты, которые можно использовать как дидактический материал не только при изучении С++, но и других языков программирования.
УДК 004.432 045С++(075.8)
ББК 32.973.26-018.1я73-1
© БГУ, 2012
ПРЕДИСЛОВИЕ
Пособие отражает многолетний опыт преподавания автором методов программирования на механико-математическом и других факультетах Белгосуниверситета, в Академии последипломного образования и других учебных заведениях.
Во второй части книги на большом количестве упражнений, тестов, примеров и задач в виде отлаженных программ дано описание простых типов данных, указателей, одномерных и двумерных массивов и показана их связь с указателями в современном языке С++.
В книге используется простой, учебный стиль изложения, принцип которого “просто о сложном”, а не наоборот. Несмотря на то, что С++ — профессиональный язык, благодаря методике изложения материала от читателя не требуется предварительных знаний по какому-нибудь языку. Теоретический материал рассматривается одновременно с разработкой программ. Книга не столько описывает язык, сколько учит программировать, что не всегда одно и тоже. Повышенное внимание уделяется вопросам, развивающим алгоритмическое и логическое мышление, а не техническим деталям, которые можно найти в справке. Подробно описаны темы, недостаточно освещённые в литературе: битовые операции и, главное, их использование; классификация типов задач и алгоритмов для работы с одномерными массивами и матрицами; и др.
В пособии при изложении теоретического материала, а также в конце каждой главы приведено большое количество решённых примеров в виде алгоритмов и отлаженных программ с подробными комментариями. Все они проверены и протестированы в консольном режиме системы С++ Builder. Опыт использования приведенных здесь программ при работе со студентами показал, что за редким исключением абсолютное большинство программ можно использовать и в системе Visual C++.
Для творческого изучения и закрепления материала в конце каждой главы приведены упражнения и тесты, т. е. разработка или анализ части программы или элемента языка (операции, оператора и т. п.). Задания для составления и отладки программ (задачи) разделены на два или три уровня сложности.
Пособие предназначено, прежде всего, студентам высших и средних учебных заведений и школьникам, изучающих языки программирования С и С++. Оно будет полезно и профессиональным программистам. Преподаватели найдут дидактический материал для лекций и практических занятий, организации самостоятельной работы и различных форм контроля знаний.
Глава 6
ПРОСТЫЕ ТИПЫ ДАННЫХ
Целый тип
Битовые операции
В С++ есть следующие битовые операции: & битовое и; | битовое или; ^ исключающее или; ~ дополнение; >> сдвиг вправо на указанное количество разрядов (бит); << сдвиг влево на указанное количество разрядов (бит). Первые три операции являются бинарными. Они работают с двоичным представлением двух операндов (констант, переменных или выражений). Они выполняются над каждой парой битов по тем же правилам, что и логические операции:
& | ^
1 1 1 1 0
1 0 0 1 1
0 1 0 1 1
0 0 0 0 0
Пример 1. Определить результат: short int k1=30,k2=-1707,r; r=k1 & k2;
cout<<endl<< k1<< “&”<<k2<<”=”<<r;
1. Представляем положительное число 30 в оперативной памяти. Для этого переводим его в двоичную систему счисления делением на 2 следующим образом: 30/2=15 (0 в остатке), 15/2=7 (1 в остатке), 7/2=3 (1), 3/2=1(1), 1/2=0(1). Записывая все остатки в обратном порядке, получим 3010 =111102. Представляем это число в двух байтах, так как оно объявлено как short int. Для этого дописываем слева нули и получаем 0000000000011110.
2. Представляем отрицательное число –1707 в оперативной памяти в дополнительном коде. Для этого выполняем следующее:
2.1) так как делением на 2 перевод в двоичную систему счисления выполняется долго, сначала переведём число 1707 во вспомогательную шестнадцатеричную систему счисления делением на 16. 1707/16=106 (11 в остатке), 106/16=6 (10), 6/16=0(6). Записывая все остатки в шестнадцатеричной системе счисления в обратном порядке, получим 6AB16, так как 1010=A16, 1110=B16;
2.2) каждую шестнадцатеричную цифру полученного результата записываем в виде четырёх двоичных цифр (двоичной тетрады): 011010101011;
2.3) представляем это число в двух байтах, так как оно объявлено как short int. Для этого дописываем слева необходимое количество нулей: 0000011010101011. Получили представление положительного числа в памяти компьютера;
2.4) для получения дополнительного кода выполняем инверсию положительного представления с учётом “левых” нулей, т. е. нуль меняем на единицу, единицу на нуль. К результату инверсии прибавляем единицу. Получим 1111100101010101.
3. К полученным таким образом двоичным кодам двух чисел применяем битовую операцию и (&). При этом единицы получаем только там, где были обе единицы, во всех остальных разрядах получатся нули: 0000000000010100.
4. Что это за число в десятичной системе счисления? Разбиваем его на тетрады и получаем число в шестнадцатеричной системе счисления: 1416. Для перевода в десятичную систему счисления записываем его как сумму степеней числа 16, умноженную на соответствующую шестнадцатеричную цифру: 1416=1*16+4=2010. При этом действия выполняются в десятичной системе счисления.
Пример 2. Найти r2=30| -1707, если a) short r2, b) unsigned short r2.
По умолчанию подразумевается тип int. К полученным в предыдущем упражнении двоичным кодам применяем битовую операцию или ( | ). При этом нули получаются только там, где были оба нуля, во всех остальных разрядах получатся единицы: 1111100101011111.
a) Так как результат объявлен как знаковое число (по умолчанию signed) и в самом левом бите получилась единица, то это дополнительный код отрицательного числа. Для его получения выполняем в обратном порядке то, что делали в пункте 2) предыдущего упражнения: вычитаем единицу и выполняем инверсию. Получим 0000011010100001. Разбив на тетрады, получаем 6A116=6*162+10*16+1=1697. Ответ: –1697.
b) Так как при таком объявлении (unsigned) отрицательного числа быть не может, то мы имеем положительное число, несмотря на единицу в самом левом бите. Разбиваем двоичное число на тетрады и переводим его в десятичную систему счисления F95F16=15*163+9*162+5*16+15=63839.
Пример 3.Пусть short r3. Определить результат: r3=~30.
Операция дополнение (~) меняет единицу на нуль, а нуль на единицу. Поэтому к полученному в упражнении 1 двоичному коду числа 30 0000000000011110 применяем инверсию: 1111111111100001. Так как по умолчанию переменная имеет модификатор signed и в самом левом бите получена единица, то это отрицательное число. Для его восстановления вычитаем единицу и выполняем инверсию: 0000000000011111=1F16=1*16+15=31.
Операция << (сдвиг влево) имеет общий вид a<<m, где a — число, участвующее в сдвиге, а m показывает, на какое количество разрядов сдвигаем число. При этом как для положительных, так и для отрицательных чисел “вращение”, или циклический сдвиг, не выполняется. Значения левых сдвинутых бит теряются, а справа появляются нули.
Пример 4. char k= –5; for(int I=1; I<=4; I++)
{ k<<=1; // или k=k<<1;
printf("%5d",k); }
Получим –10, –20, –40, –80. Почему? Число –5 имеет следующее представление в одном байте (тип char): 11111011. После сдвига на один разряд имеем 11110110. При этом левая крайняя единица потерялась, и справа появилось число 0, а не 1. Так как это отрицательное число, то после вычитания единицы и инверсии получаем 00001010, т. е. число -10. Аналогично получаем остальные числа.
Сдвиг влево на один разряд означает увеличение целого числа в два раза, сдвиг влево на два разряда увеличивает число в четыре раза и так далее, сдвиг влево на m разрядов равносилен увеличению числа в 2m раза. Это не зависит от того, какое число, положительное или отрицательное, участвовало в сдвиге.
Сдвиг вправо (a >> m) выполняется аналогично, без циклического сдвига. При сдвиге положительных чисел слева появляется m нулей, а при сдвиге отрицательных чисел слева добавляется m единиц.
Сдвиг вправо на один разряд означает целочисленное деление на 2, сдвиг вправо на два разряда означает целочисленное деление на 4 и так далее, сдвиг вправо на m разрядов равносилен целочисленному делению числа на 2m
Пример 5.a) 42>>2 даёт 1010. Действительно, 42=001010102, а в результате сдвига получаем 000010102=1010.
b) В результате операции –42>>2 получим –11. Почему? –42=110101102, а после сдвига получим 111101012, а это отрицательное число–11.
В качестве упражнениявыполните следующий фрагмент программы и объясните результат:
char k= –30; cout<<endl;
for(int I=1; I<=4; I++) { k>>=1; // или k=k>>1;
printf ( "%5d" , k); }
Логический тип
В классическом “старом” языке С такого типа не было. Вместо него использовался целый тип. Логический или булевый тип появился в языке С++. Переменные такого типа объявляются, например, следующим, образом: bool B1, B2, B3, B4. Переменная данного типа может принимать одно из двух значений — true или false, которыеявляются логическими константами. Кроме этих переменных дальше в тексте будем использовать вещественные переменные x и y, определяющие координаты точки на плоскости в декартовой системе координат.
Логические переменные можно использовать:
· в операторах присваивания, например,
B1=x>0; B1=true; B2=y>0 && x<0;
B1=B2 || x*x+y*y<=4;
· в операторе if, например, if (B1), что равносильно if (B1= = true) …;
· в операторах while и do … while,
например, while (B1) { …}, что равносильно while(B1= = true) {…};
· в заголовке оператора for. Чаще всегобулевский тип используется во второй части заголовка. Например, for (x=1; B1; x+=0.5) {…}; что равносильно for (x=1; B1==true; x+=0.5) {…}; Теоретически переменные булевского типа могут быть в первой и в третьей частях заголовка этого оператора.
Для логических переменных и констант определены логические операции &&, || , ! и операции сравнения > , < , >=, <=, = =, != (см. 3.3 гл. 1). Например, присваивание B3 = y>x*x && fabs(x)<2 можно записать так:
B1= y>x*x; B2= fabs(x)<2; B3=B1 && B2;
При сравнении логических величин учитывается, что результаты следующих сравнений истинны: true>false; true = = true; false = = false.
Пример 13. Пусть B1=x>0; B2=y>0;
a) Выражение B1>B2 истинно тогда и только тогда, когда x>0 && y<=0 истинно, т. е. когда B1==true, а B2==false.
b) Выражение B1== B2 истинно тогда и только тогда, когда x>0 && y>0 || x<=0 && y<=0, т. е. когда B1==true и B2==true или обе переменные принимают значение false.
c) Выражение !(B1<B2) равносильно
x>0 && y<=0|| x>0 && y>0 || x<=0 && y<=0.
Существует связь между логическими операциями и оператором if.
Пример 14. Присваивание B3=B1 && B2 с помощью if без логических операций равносильно if (B1) B3 = B2; else B3 = false;
Пример 15. Дан вложенный if
if (B2) B3=true;
else if (B1) B3 = false;
else B3 = true;
Это можно записать короче: B3=!B1 || B2;
Мы уже знаем, что целый тип совместим с булевским типом. Любое ненулевое число играет роль true, а нуль равносилен false. Поэтому если int flag, то оператор if (flag) cout<<”Yes”; else cout<<”No”; равносилен следующему:
if (flag!=0) cout<<”Yes”; else cout<<”No”.
Надо различать логические и битовые операции.
Если логические операции используются для целых чисел, то любое ненулевое число играет роль true, а нуль равносилен false.
Пример 16. В результате выполнения операторов
int b1=10<0xA, b2=0x10, b3; b3=b1 || b2 && 10; cout<<b3;
выведем единицу. Почему? Так как 10 и 0xA — одно и то же число в разных системах счисления, то результат сравнения ложный и b1=0. Значением переменной b2 является ненулевое число 16 в шестнадцатеричной системе счисления, а это соответствует истине. Поэтому b2 && 10 даст в результате истину, и b3 также будет истинным.
Если объявить bool b1=10<0xA, b2=0x10, b3; то результат будет тот же самый, т. е. будет выведена единица (а не true).
Наоборот, битовые операции можно использовать для логических значений, что на практике используется редко.
Пример 17. Рассмотрим похожий на предыдущий пример с битовыми операциями: bool b1=10<0xA, b2=0x10, b3; b3=b1 | b2 & 10; cout<<b3;
В результате операции b2 & 10 получим 0, потому что битовая операция применяется к каждой паре битов, т. е. 12 & 10102=0. Значением b1 является false, т. е. 0. Поэтому в результате будет выведен 0. Необходимо обратить внимание, что значением переменной b2 будет 1, а не 100002 . По этой же причине, если bool b2=10, то в результате получим 0.
Но если int b1=10<0xA, b2=10, b3; то получим 10, так как 10&10=10, b1=0, 0 | 10 =10.
Символьный тип
Мы уже знаем, как объявить переменную символьного типа, правила записи символьных констант. Каждый символ занимает один байт.
Для ввода одного символа можно использовать функции getchar, getch или getche. От их выбора зависит, надо ли при вводе символа нажимать не только на клавишу вводимого символа, но и дополнительно на “Ввод” (да для getchar, нет для getch, и нетдля getche). Во-вторых, эти стандартные функции влияют на отображение набираемого на клавиатуре символа на экране (да, нет, да).
Для вывода символа вместо cout удобнее использовать функции printf или cprintf, так как с помощью форматов (“%c” или “%d”) можно указать, что мы выводим, символ или его код (см. пример 19).
Среди символов есть такие, которые нельзя ввести обычным способом, так как их нет на алфавитно–цифровой клавиатуре. Они называются символами псевдографики.Для их ввода курсор подводим в нужное место и нажимаем “Alt”. При включённом цифровом режиме (клавиша NumLock) код символа набираем справа на клавиатуре. Во время набора ничего не отображается, но когда отпустим клавишу “Alt”, в соответствующем месте появится символ псевдографики с набранным кодом. Такие символы в функцию можно передать, как и обычные символы, двумя способами: с помощью его числового кода или вышеописанным способом набрать такой символ.
Пример 18. Составить и проверить функцию, которая рисует границу прямоугольника. Параметры функции: x0, y0 — текстовые координаты левого верхнего угла; n1,n2 — размеры прямоугольника.
void ramka (int x0, int y0, int n1, int n2)
{ int x,y,i; x=x0; y=y0;
gotoxy(x,y++); printf("%c",218);
for (i=0;i<n1-2;i++) { gotoxy(x,y++); printf("%c", 179); }
gotoxy(x++,y); printf("%c",192);
for (i=0;i<n2-2;i++) { gotoxy(x++,y); printf("%c",196); }
gotoxy(x,y--); printf("%c",217);
for(i=0;i<n1-2;i++) { gotoxy(x,y--); printf("%c",179); }
gotoxy(x--,y); printf("%c",191);
for(i=0;i<n2-2;i++) { gotoxy(x--,y); printf("%c",196); }
}
int main ()
{ ramka (2,5, 10, 30); getch(); return 0; }
В программе использовались кодыследующих символов псевдографики:
218 — для левого верхнего угла;
179 — для вертикальной линии;
192 — для левого нижнего угла;
196 — для горизонтальной линии;
217 — для правого нижнего угла;
191 — для правого верхнего угла.
Пример 19. Другие особенности символов можно увидеть, выполнив программу для вывода кодовой таблицы. то есть всехсимволов, в том числе и символов псевдографики, и их кодов:
main()
{ clrscr(); textcolor(10);
for (int i=1; i<256; i++)
{ cprintf("%4d",i); printf("%c%c" , i, i%12?' ':'\n'); }
getch(); return 0;
}
В цикле по формату "%4d” выводим код символа указанным в textcolor 10-м цветом, а по формату "%c” — символ с этим кодом цветом по умолчанию. Если i не кратно 12, т.е. i%12 != 0, то выводим пробел. В противном случае с помощью '\n' переходим на следующую строку экрана.
Анализ кодовой таблицыпозволяет отметить следующие особенности символов:
· цифры в таблице располагаются подряд, цифра 0 имеет код 48, у цифры 1 код 49, и т. д., цифра 9 имеет код 57;
· как прописные, так и строчные латинские буквы располагаются непрерывно в алфавитном порядке с кодами 65—90 для больших и 97—122 для маленьких букв;
· русские прописные буквы располагаются также подряд (коды 128—159);
· маленькие русские буквы терпят разрыв, буквы от ‘а’ (код 160) до ‘п’ (175) следуют подряд. После буквы ‘п’ размещаются символы псевдографики, а затем русский алфавит продолжается с буквы ‘р’ (224) до ‘я’(239).
Рассмотрим совместимость символьного и целого типов,что характерно для языка С++. В переменной типа char можно хранить как символы (точнее, их коды), так и целые однобайтные числа. Для переменных типа char определены те же операции, что и для типа int, например, получение остатка от целочисленного деления (%), сложение и другие.
Пример 20. В языке С++, в отличие от Pascal, можно выполнить следующий фрагмент: char a='5', b=5; cout<<a<<b << a%b<<" "<<a/b;
В результате выведем символ 5, символ с кодом 5 (см. кодовую таблицу), 3, 10. Почему? В переменной a хранится код символа ‘5’, т. е. целое число 53 (48+5, где 48 — код нуля), а в переменной b — обычное целое число 5, а 53 % 5 = 3, 53 / 5 = 10. Заметим, что если объявим int a=53, b=5; то выведем 53, 5, 3, 10. Если, что интереснее, int a=’5’, b=5; то получим то же, что и в предыдущем варианте, т. е. 53, 5, 3, 10, так как значением целочисленной переменной a будет код символа 5 — число 53.
При вводес помощью cin результат зависит от того, как переменная объявлена и что вводим.
Пример 21. Пусть char a, b; cin>>a>>b;
a) При выполнении введём 5 и 3. Тогда с помощью того же cout получим 5, 3, 2, 1. Здесь число 2 получено не как остаток от деления 5 на 3, а как результат операции % для кодов этих символов, т. е. 53 % 51 = 2. Для этих же чисел получено и число 1. В этом можно убедиться, если вместо частного вывести произведение cout << a*b; На экране мы увидим не 15 (5*3), а 2703, т. е. произведение кодов символов 5 и 3 (51* 53 = 2703).
b) Пусть по–прежнему char a, b; cin>>a>>b; При выполнении наберем два символа, например, a и с, коды которых 97 и 99. Тогда по той же причине будет выведено: a, c, 97, 9603, где 97=97%99, а 9603=97*99. “Числа”, которые больше, чем 9, при таком объявлении ввести не сможем. Если введём, например, 123, то a примет значение символа ‘1’, b= ‘2’, а символ 3 игнорируется. Получим 1, 2, 49, 2450, где 49=49 % 50, 2450 = 49*50.
с) Если объявлено int a, b; и вводим целые числа, то значениями этих переменных будут введённые числа. При вводе символов ничего хорошего не получим, хотя программа будет выполняться.
Вещественный тип
Вещественное число (число с плавающей запятой) состоит из двух частей: мантиссы и порядка. Например, число в десятичной системе счисления 0,000123 можно записать одним из следующих способов: 0.0000123*10; 0,123*10-3; 1,23*10-4 и т.д. Аналогично 78900=0,789*105=7,89*104=78,9*103 и т.д. Термин “число с плавающей запятой” и связан с тем, что десятичная запятая в различных записях числа перемещается (плывёт) по числу. Нас будет интересовать нормализованное число (соответственно 0,123*10-3 и 0,789*105 ), в котором в левом числе после запятой значащая цифра, а не нуль. Первая его часть называется мантиссой(0,123 и 0,789), а числа -3 и 5 – порядком.
Аналогично различные варианты записи на бумаге, а не в памяти компьютера вещественного числа имеют место и в двоичной системе счисления.
Пример 22. Рассмотрим десятичное число 12,375. Для его перевода в двоичную систему счисления отдельно переводим целую часть аналогично как было показано в первом параграфе и отдельно дробную часть. В качестве вспомогательной системы счисления можно использовать шестнадцатеричную. Для перевода дробной части из десятичной системы счисления в шестнадцатеричную выполняем следующее:
дробную часть числа умножаем на 16;
полученную целую часть результата (число от 0 до 15) переводим в шестнадцатеричную систему счисления и берём в качестве первой после запятой 16-й цифры результата;
дробную часть результата, если она не равна нулю, повторно умножаем на число 16;
полученную целую часть переводим в шестнадцатеричную систему счисления и берём в качестве следующей шестнадцатеричной цифры;
дробную часть результата снова умножаем на 16.
Этот процесс продолжаем, пока не наступит одна из следующих ситуаций:
a) на некотором шаге, не обязательно в самом начале, получим в дробной части нуль. В этом случае перевод выполнили точно. Это имеет место в нашем примере: 0,375*16=6.0;
b) получим в дробной части число, которое было раньше. Например, 0,15*16=2,4; 0,4*16=6,4. Если продолжать умножение 0,4*16, будем получать одно и то же, т. е 6,4. В таком случае получаем следующий результат: 0,1510= 0,2666…16=0,2(6)16. Круглые скобки означают, что записанное в них одно или несколько разных чисел будут повторяться бесконечное число раз. Говорят, что это число в периоде, т.е. 6 в периоде;
с) если не получаем ни нуль, ни повторяющиеся числа, то ограничиваемся заданным предварительно количеством двоичных или шестнадцатеричных цифр. Для числа типа float необходимо получить 24 двоичные цифры, считая от первой значащей, или не менее семи шестнадцатеричных цифр, не считая первые нули. Немного позже в этом же параграфе будет понятно, почему такое количество цифр надо получать.
Для перевода дробной части из шестнадцатеричной системы счисления в десятичную записываем каждую шестнадцатеричную (но не десятичную!) цифру в виде тетрады, т.е. четырёх двоичных цифр. Получим 12.37510=С.616=1100,0110=1100,011. При этом последнюю цифру ‘0’ можем не писать. Как и в десятичной системе счисления, этот нуль незначащий. Остальные нули рядом с запятой как слева, так и справа, обязательны! Это двоичное число, как и в десятичной системе счисления, записать можно по-разному: 11,00011*22; 1100011*2-3; 1.100011*23 и т. д. Из приведенных вариантов нас будет интересовать последняя нормализованная запись, в которой в целой части записана одна первая значащая единица. Получим: m=0.100011; p=310=112, где m — мантисса, p — порядок в двоичной системе счисления.
Пусть переменная объявлена как float. Тогда 4 байта или 32 бита распределяются так.
Один самый “левый” бит отводится под знак мантиссы, или, что то же самое, под знак всего числа. Записывается 0, если мантисса, а, значит и само вещественное число, положительное, и 1 в противном случае. Никакого дополнительного кода для отрицательного вещественного числа, как это было для целых чисел, получать не надо.
Следующие 8 разрядов (бит) занимает изменённый порядок записи числа в двоичной системе счисления, который называется характеристикой числа (обозначим её x). Что это такое? Знак порядка нигде не хранится. Чтобы он всегда был неотрицательным, порядок увеличивается на 12710, т. е. x=p+12710=p+7F16. (1) Для нашего примера здесь будет храниться число x=310+12710= 13010=8216=100000102.
Это же можно вычислить и так: x=316+7F16=8216 =100000102 ;
Последние 32-1-8=23 разряда занимает мантисса. При этом старшая её цифра, равная 1, в памяти не хранится, но учитывается при вычислениях. Если дробная часть числа переведена в шестнадцатеричную, а, значит и в двоичную системы счисления не точно, т. е. имели место два последних варианта (см. выше перевод), последняя двоичная цифра округляется по обычным правилам. Если первая отбрасываемая двоичная цифра равна 1, то прибавляем двоичную единицу, в противном случае оставляем без изменения.
Таким образом, число 12,375 в формате float будет представлено следующим образом: 01000001010001100000000000000000. В литературе можно встретить шестнадцатеричную запись результата: 4146000016.
Пример 23. Представить число -0.01 как число с плавающей точкой в формате float.
Переводим дробную часть модуля числа в двоичную систему счисления: 0.01=0.028F5С28F5C…16=0.0(28F5С)16=
0.0000001010001111010111000010100…2. Так как под мантиссу отводится 23 разряда, то должны получить 25 двоичных цифр, не считая первых после десятичной точки подряд идущих нулей. Почему? По правилу нормализации самая первая значащая единица (в примере в десятичной цифре 2) в память не записывается, а ещё одна дополнительная двоичная цифра нужна для того, чтобы определить, как округлять число. Так как первая отбрасываемая двоичная цифра = 0, то получаем
m=0.010001111010111000010102. Если число “маленькое”, т.е. целая часть = 0, а в дробной части после запятой несколько подряд идущих нулей, то получим отрицательный порядок. Так как 0.0000001010001111010111000010102= 1.01000111101011100001010*2-7,
то p=-710=-716, x= p+7F16=7F16-716=7816=011110002. В результате получим ответ: 10111100001000111101011100001010.
Пример 24. Рассмотрим обратную задачу. Пусть в ячейке размером 4 байта хранится следующая последовательность нулей и единиц, шестнадцатеричное представление которой такое: С215999A16. Известно, что здесь хранится вещественное число, т.е. в программе записано объявление: float a. Что это за число в 10-й системе счисления?
Для ответа на этот вопрос в обратном порядке выполняем действия, описанные выше.
1) Запишем двоичное представление содержимого ячейки: 11000010000101011001100110011010.
2) Единица в старшем бите (самая “левая”) означает, что всё вещественное число отрицательное.
3) В следующих 8 битах находится характеристика числа, т.е. x=100001002=8416. Из формулы (1) получаем двоичный порядок числа:
p=x-7F16=8416-7F16 =516=510.
4) Из последних 23 разрядов получаем
m=0.001010110011001100110102.
5) Поэтому искомое число a=1.00101011001100110011010*25=
100101.0110011001100110102@25.(6) 16@37.410.
Перевод дробной части из шестнадцатеричной в десятичную систему счисления выполняли следующим образом: 0.(6) 16 = 0.6666616 = 6*16-1+6*16-2+6*16-3+6*16-4+6*16-5@0.410.
Т.к. в самом “левом” бите была единица, то получаем ответ: - 37.410.
§ 5. Преобразование типов
5.1. Преобразование типов в выражениях
Такое преобразование имеет место, когда в выражении смешиваются переменные и константы различных типов. Тогда компилятор преобразует все операнды (элементы выражения) к типу большего операнда (с точки зрения памяти) по следующим правилам:
переменные типа char и short int преобразуются к типу int;
переменные типа float преобразуются к типу double;
если один из пары операндов имеет тип double, другой операнд также преобразуется к double;
если один из пары операндов имеет тип unsigned, другой операнд также преобразуется к unsigned;
Пример 25. Пусть char c; int k; float f; double d, res; … res=(f+d) * (c-k);
Последовательно выполняется преобразование внутри каждой пары операндов до выполнения соответствующей операции. Перед выполнением сложения переменная f преобразуется к типу double и затем суммируются переменные одинаковых типов double. Перед вычитанием с преобразуется к типу int и результат вычитания будет целым. Так как (f+d) имеет тип double, то перед умножением целое (c-i) преобразуется к типу double, и всё выражение будет иметь тип double. Так как переменная слева имеет такой же тип double, то во время присваивания преобразование типов в этом примере не выполняется (см. 5.2).
Есть возможность “заставить” выражение принять определённый тип с помощью операции принудительного преобразования, имеющей следующий вид: (тип) выражение. Здесь в скобках можно записать стандартный или определённый пользователем тип.
Например, часто можно встретить следующую ошибку. Пусть переменная Sum используется для вычисления суммы элементов целочисленного массива размерности n, где n, конечно, целое. Тогда если объявить int Sum, то среднее значение по формуле Aver=Sum/n; будет найдено неправильно, даже если float Aver. Почему? Несмотря на то, что Aver вещественное, мы имеем дело с целочисленным делением, так как и Sum, и n целые. Поэтому при делении будет отброшена дробная часть результата,. Полученное целое значение будет преобразовано к типу float без восстановления отброшенной дробной части и присвоено переменной Aver.
Один из способов преодолеть это — объявить float Sum. А если нам надо, чтобы в других операторах этого же блока или функции переменная Sum была целой? Тогда, оставив int Sum, записываем Aver=(float)Sum/n. В таком случае при вычислении частного произойдёт “временное” преобразование Sum из int во float, а в остальных операторах как до, так и после этого присваивания Sum будет иметь тот тип, который записан при объявлении, т. е. целый. Поэтому если, например, записать cout << Sum; то сумма будет выведена как целая величина.
Пример 26.Дан код: float f=100.5; cout<<(int)f/6<<” “ << int)f/6.<< " "<<f/6<< " "<<f/6.; Что будет выведено?
Ответ: 16 16.6667 16.75 16.75
Объяснить результат.
5.2. Преобразование типов при присваивании
Что получится, если бы в предыдущем примере res=(f+d) * (c-i); переменная res имела тип, отличный от double, например, целый? Тогда используется следующее правило преобразования типов при присваивании: значение справа от оператора присваивания (значение выражения) преобразуется к типу переменной, стоящей слева. В результате получилась бы целая часть выражения справа.
Пример 27. Пусть char c; int k; float f; double d;
a) При присваивании c=k в переменной c остаются младшие 8 битов переменной k. Когда происходит преобразование из целого числа int или short к символу char, из целого int— к короткому целому short, то старшие биты теряются.
b) В результате присваиваний f=c; f=k; получается целое число, преобразованное к формату с плавающей точкой.
При преобразовании из int в float или из float в double точность не увеличивается, а всего лишь меняется формат представления данных.
Пример 28. Дан код:
float f2 =100.5, r1=(int)f2/6, r2=(int)f2/6., r3=f2/6, r4=f2/6.;
cout<<r1<< " "<<r2<<" "<<r3<< " "<<r4;
Что будет выведено?
Ответ: 16 16.6667 16.75 16.75
Объяснить, какие преобразования выполнены.
Пример 29. Дан код:
int f2 =100.5, r21=(int)f2/6, r22=(int)f2/6., r23=f2/6, r24=f2/6;
cout<<r21<<" "<<r22<<" "<<r23<<" "<<r24;
Что будет выведено?.
Ответ: 16 16 16 16
Объяснить, какие преобразования выполнены.
Упражнения и тесты
Целый тип. Битовые операции.
1. Что будет выведено? int a=170, r = a & 15; cout<<r;
r = a | 8; cout<<endl<<r<<endl;
if ((a & 8) == 8) cout<<"Yes"; else cout<<"No";
Решение. Число 170 переводим в шестнадцатеричную систему счисления делением на 16. Получим 17010=AA16. Каждую шестнадцатеричную цифру запишем в виде двоичной тетрады и добавим необходимое количество незначащих нулей. Получим 00000000000000000000000010101010.
1510 = 11112. Учитывая правила выполнения операции &, выведем десятичное число 10, т. е. с помощью этой операции всегда выделяется последняя шестнадцатеричная цифра числа.
Так как 810=10002 и принимая во внимание правила выполнения операции | (битовое или), получим то же число 170.
Так как в двоичном представлении числа 8 есть всего одна единица, то результатом операции a & 8 будет либо 0, либо 8 =10002 в зависимости от того, что находится в 3–м справа бите числа a (нумерация битов с нуля). В нашем примере получим 8, и будет выведено “YES”, что означает, что в 3–м справа бите исходного числа a была единица.
2. Определить результат:
unsigned short a; cin>>a; a=a & 0xFFF7; cout<<a;
Решение. Введем, например, число 26. В двоичной системе счисления это 0000000000011010. Так как FFF716=11111111111101112, то операция &(битовое и) оставит без изменения все биты, кроме третьего справа. Независимо от того, что было в этом бите (0 или 1), получится нуль, т. е. мы “выключили” третий справа бит. В результате получим 00000000000100102=18, т. е. будет выведено число 18. Если введем, например, число 23, то это число не изменится, так как в третьем бите был 0.
3.Пусть int a=-8, b=23. Что получится в результате выполнения следующей части программы: printf (“%d %X %d %X”, a & b,a | b,a ^ b,~a);
4. Найти значение переменной r, объявленной как int r, если:
а) r= ~(100>>2) & ~(–10) | 0x10; b) r=162 | 0x10A & 111;
c) r= –10 & 1|4; d) r=(123<<4)& 123.
5. Найти значение этого же выражения из предыдущего упражнения, если переменная объявлена как unsigned short r.
6.Найти r3=30 ^ –1707, если:
a) short r3, b) unsigned short r3.
7a. Пусть в ячейке, объявленной short a, хранится следующая двоичная последовательность:
а) 0000001001001001; b) 1100000000001000;
c) 1111111111111000; d) 1000000000000000.
Что это за число в десятичной системе счисления? шестнадцатеричной системе счисления?
7b. Выполнить предыдущее упражнение, если переменная объявлена как unsigned short a.
8. Как включить 4-й справа бит, т. е. поместить туда единицу независимо от того, какое там было значение? Нумерация битов с нуля.
9. Как заменить значение 2–го справа бита на противоположное значение, т. е. нуль на единицу, а единицу на нуль? Нумерация битов с нуля.
Логический тип.
10.Пусть bool b1, b2, b3. Записать оператор if и операторы присваивания, которые выполняют те же действия, что и оператор
a) b1= b2 || b3; b) b1=b2 || b3 && b4;
c) b1=(b2 || b3) && b4; d) b1= !(b2 && b3 || b4).
Решение: a) if (b2) b1=true; else b1=b3;
11. Записать с помощью логических операций и оператора присваивания,не используя оператор if:
bool b1, b2, b3,r; if (!b1) r=false; else if (!b2) r=false; else r=b3;
12. Нарисовать область плоскости, в которой и только в которой для вещественных величин x и y следующее логическое выражение истинно:
а) (fabs(x) < 1) > (fabs(y) < 1);
b) ! (fabs(x) < 1 ) = = (fabs(y) < 1);
c) ! (( fabs(x) < 1 ) = = (fabs(y) < 1)).
Решение. a) Надо найти область плоскости, в которой и только в которой одновременно первое неравенство истинно, а второе ложн