Программирование в Linux. Учебное пособие С. В. Шапошникова, Лаборатория юного линуксоида, май 2012 1 Пояснительная записка
Скачать 0.88 Mb.
|
Инкремент и декремент Прежде, чем изучать циклы, следует познакомиться с часто используемым в языке C способом увеличения/уменьшения значений переменных на единицу. Конечно, в C работают такие формы изменения значений как, например, a += 1 или a -= 1 . Однако чаще используют операции инкрементирования (оператор инкремента "++") и декрементирования (оператор декремента "--"): i++ или ++i , i-- или --i . В результате этих операций переменные увеличиваются или уменьшаются на единицу. Запомните, когда вы видите выражения типа ++i или i++ , то в результате их выполнения значение i меняется. Не надо делать вот так: i = ++i . Это совершенно лишнее. Когда знак инкремента или декремента стоит перед переменной, то перед нами префиксная форма операции ( ++i, --i ), а когда после переменной, то постфиксная форма ( i++, i-- ). Когда эти выражения не участвуют в построении более сложных выражений, то между префиксной и постфиксной формами никакой разницы нет: что i++ , что ++i — без разницы, в результате мы получим значение i на единицу больше. Но когда эти выражения участвуют в построении более сложных, то разница между префиксной и постфиксной формами появляется и заключается в следующем: переменная над которой производится операция инкрементирования или декрементирования в постфиксной форме сначала используется в сложном выражении как есть, и только потом увеличивается на единицу; если мы имеем дело с префиксной формой, то переменная сначала изменяется, а затем используется. Например, код: int a, b, c, d; a=b=c=d=0; // выражение означает, что всем переменным присваивается 0 printf("a=%d, b=%d, c=%d, d=%d\n", a, b, c, d); c = ++a; d = b++; printf("a=%d, b=%d, c=%d, d=%d\n", a, b, c, d); , выведет на экране: a=0, b=0, c=0, d=0 19 a=1, b=1, c=1, d=0 Объясняется такой результат так: • значение переменной a было увеличено на единицу, после чего это значение было присвоено переменной c; • значение переменной b было сначала присвоено переменной d и только потом увеличено на единицу. Еще один пример: int x, y; x = y = 0; printf("%d\n", x++ > 0); printf("%d\n", ++y > 0); На экране будет выведено: 0 1 Это результат логических выражений, где 0 означает ложь, а 1 — истину. В данном случае, когда x сравнивается с нулем, то его значение еще не увеличено, а когда сравнивается у, то его значение уже больше нуля. Применять операторы инкремента и декремента можно также к переменным вещественного типа. Задание Придумайте более сложные, чем представленные выше, выражения с использованием операторов инкремента и декремента. Проверьте и объясните результат их выполнения. while Цикл while в языке программирования C работает также как и в других языках программирования. По аналогии с условным выражением в инструкции if , условное выражение при while заключается в круглые скобки. Если тело цикла включает несколько выражений разделяемых точкой с запятой, то все тело заключается в фигурные скобки. Задание 1. Присвойте переменной star значение 0. Пока значение star не достигнет 55 выводите на экран в строку по одной звездочке (*). 2. С помощью цикла while запрограммируйте вывод на экран цифровых кодов и значений таблицы символов ASCII от 31 до 127 включительно. При этом после каждого десятого символа осуществляйте переход на новую строку. (Подсказка: чтобы переходить на новую строку, в цикле while надо использовать инструкцию if, в условии которой остаток 1 от деления на 10 сравнивается с нулем.) 20 3. Используя внешний и вложенный циклы while организуйте вывод таблицы умножения на экран. 1 Операция нахождения остатка от деления в языке C обозначается знаком процента (%). do-while Цикл do-while отличается от while лишь тем, что его тело будет выполнено хотя бы один раз независимо от условия выполнения цикла. Синтаксис цикла do-while можно описать так (фигурные скобки можно опустить, если законченное выражение только одно): do { выражение1; …; } while (логич_выражение); Этот цикл называют циклом с постусловием. Его используют намного реже обычного while В принципе почти всегда можно обойтись без него, но в определенных ситуациях его использование упрощает код. Допустим требуется вывести на экран отдельные цифры числа. Любое число состоит хотя бы из одной цифры, даже число 0. Можно решить эту задачу с использованием цикла while : while (a > 0) { printf("%d\n", a % 10); a = a / 10; } Но в этом случае, если a равно 0, то цикл не выполнится ни разу. Пришлось бы перед циклом использовать инструкцию if , в которой сравнивать переменную с 0. Использование же цикла do-while решает эту проблему, т.к. его тело один раз выполнится даже при нулевом значении переменной: do { printf("%d\n", a % 10); a = a / 10; } while (a > 0); Задание Придумайте и напишите любую программу, в которой бы использовался цикл do-while. for Представим синтаксис заголовка цикла for языка программирования C так: for (часть1; часть2; часть3) Заголовок цикла for включает три части, разделенных точкой с запятой; причем каждая 21 часть может быть сложной, т.е. состоять из нескольких выражений, разделенных простой запятой. В первой части обычно указываются переменные и часто их начальные значения; во второй - с помощью логического(их) выражения(й) задаются условия, при которых выполняется тело цикла; в третью часть помещаются выражения, которые выполняются в конце каждой итерации цикла (чаще всего здесь изменяется значение переменной, заданной в первой части заголовка). Вот так будет выглядеть программный код, выводящий таблицу символов на экран, в котором используется цикл for : unsigned char a; for (a = 31; a < 128; a++) { if (a % 10 == 0) printf("\n"); printf("%4d-%c", a, a); } printf("\n"); Задание Напишите программу с использованием цикла for, выводящую на экран таблицу умножения (Подсказка: как и в случае с while следует использовать два цикла — внешний и вложенный.) break и continue Оператор break позволяет прервать выполнение цикла, а continue — прервать текущую итерацию (проход) цикла. Почти всегда можно обойтись без этих операторов, но иногда их использование позволяет упростить программный код и сделать его более понятным. Рассмотрим пару примеров. Допустим, требуется проверить массив на наличие в нем хотя бы одного элемента со значением 0. Как только ноль будет обнаружен проверять оставшуюся часть массива уже нет смысла. Поэтому, чтобы не выполнять лишних итераций, используется оператор break Второй пример. Требуется из одного массива скопировать в другой только числа, которые больше 0. Можно с помощью continue прерывать итерацию цикла, если очередной элемент меньше либо равен нулю. #define N 10 int arr[N] = {6, 5, -4, 3, -7, 2, 7, 0, 3, 9}; int new_arr[N], i, j; for (i=0; i printf("Stop. Array contains zero\n"); break; } } printf("\n"); 22 for(i=0, j=0; i continue; new_arr[j] = arr[i]; printf("%d ", new_arr[j]); // проверка j++; } printf("\n"); В данном случае использование continue совсем не очевидно с точки зрения надобности, т.к. легко можно обойтись без него, если изменить условие при if на противоположное, удалить continue , а оставшийся код поместить в тело оператора if Задание Придумайте пример, в котором уместно было бы использовать оператор break или continue. Урок 5. Битовые операции Для освоения темы этого урока вам потребуются знания о системах счисления (двоичной, восьмеричной и др.), навыки перевода чисел из одной системы счисления в другую, а также вы должны иметь представление о том, что такое битовые (они же поразрядные) операции. С последним можно познакомиться по вот этой лекции В языке программирования C существуют следующие поразрядные операции: & (И), | (ИЛИ), ^ (исключающее ИЛИ), << (сдвиг влево), >> (сдвиг вправо), (поразрядное дополнение до единицы). Рассмотрим на примерах, как они работают, но перед этим уделим внимание выводу в языке C чисел в отличных от десятичной системах счисления. В С можно присваивать целочисленные значения в десятичной, восьмеричной и шестнадцатеричной системах счисления. Для того, чтобы присвоить переменной число в восьмеричной системе счисления, перед ним надо написать 0 (ноль), в шестнадцатеричной — 0x (ноль и икс), например: int a, b; a = 077; // записано восьмеричное число b = 0x1F; // присвоено шестнадцатеричное число Любые целые числа можно выводить на экран в десятичном, восьмеричном и шестнадцатеричном представлении. Пример кода для вывода определенных ранее двух переменных в различных системаъ счисления: printf("%d %o %x %X\n", a,a,a,a); printf("%d %o %x %X\n", b,b,b,b); В результате на экране вы увидите: 63 77 3f 3F 31 37 1f 1F Восьмеричные и шестнадцатеричные числа используются из-за удобства при работе с двоичной системой счисления. Каждая цифра восьмеричного числа может быть заменена тремя цифрами двоичного. И каждая цифра шестнадцатеричного числа легко заменяет четыре разряда двоичного числа. Вот таблица соответствия цифр восьмеричной системы счисления числам двоичной системы: 23 0 000 1 001 2 010 3 011 4 100 5 101 6 110 7 111 Теперь допустим, что у нас есть восьмеричное число 037. По таблице легко понять, что в двоичном выражении оно будет выглядеть как 011 111. Задание 1. Как будут выглядеть восьмеричные числа 04271 и 03566 в двоичном представлении. 2. Составьте на бумаге таблицу соответствия шестнадцатеричный цифр двоичным числам. Переведите числа 7D, FFFF, 2C9 в двоичную систему счисления. Итак, если бы мы при работе с поразрядными операциями использовали десятичные числа, то чтобы оценить результат нам бы каждый раз приходилось переводить десятичное число в двоичную систему счисления, что относительно трудоемко. Если же человек видит, например, восьмеричное число, то он может представить как оно выглядит в двоичном представлении, помня или держа перед глазами таблицу соответствия чисел. Например, как только мы видим 017, то можем представить в уме, как последние четыре бита ячейки памяти забиты единицами. Теперь вернемся к поразрядным операциям и протестируем каждую из них. Для этого напишем небольшую программу: int a, b; a = 017; b = 036; printf("0%o & 0%o = 0%o\n", a, b, a & b); printf("0%o | 0%o = 0%o\n", a, b, a | b); printf("0%o ^ 0%o = 0%o\n", a, b, a ^ b); printf("0%o << 2 = 0%o\n", a, a << 2); printf("0%o >> 2 = 0%o\n", a, a >> 2); printf("0%o = 0%o\n", a, a); Результат ее работы будет выглядеть так: 017 & 036 = 016 017 | 036 = 037 017 ^ 036 = 021 017 << 2 = 074 017 >> 2 = 03 017 = 037777777760 Этот результат будет проще понять с помощью рисунка: 24 В последнем случае получилось такое большое число потому, что под форматы вывода целых чисел (%d, %o, %X) выделяется по 4 байта. Задание 1. Используя шестнадцатеричные числа, напишите аналогичную приведенной выше программу. Объясните результат. 2. Попробуйте составлять сложные битовые операции (в несколько действий) и оценивать их результат. Теперь рассмотрим пример использования битовых операций. Допустим, у нас есть массив, требуется снять с него "маску", которая бы отражала, в какой позиции стоят отрицательные, а в какой положительные элементы. Пусть единица в бите обозначает соответствующий ей положительный элемент массива, а ноль — отрицательный. Другими словами, если у нас есть массив {4, -3, 2, 2, 8, -1}, то его "битовая маска" будет выглядеть как 101110, или в восьмеричном представлении как 056. Составим алгоритм решения этой задачи: 1. Будем считать, что массив состоит не более чем из 32 элементов. Поэтому для хранения его "маски" достаточно переменной типа int . Назовем ее mask и присвоим значение 0. 2. Перебрать элементы массива в цикле for . Если встречается положительный элемент, то установить соответствующий ему бит значения mask в 1. 3. Вывести значение переменной mask на экран в виде восьмеричного числа. Вроде бы все просто, но как установить в единицу определенный бит числа? Существует закономерность соответствия степеней двойки и двоичного представления числа: 2 0 = 0000 0001 2 1 = 0000 0010 2 2 = 0000 0100 2 3 = 0000 1000 2 4 = 0001 0000 и т.д. Если для вас эта последовательность не очевидна, то пересчитайте. Теперь если применить к mask побитовую операцию | (ИЛИ), а в качестве второго операнда использовать определенную степень двойки, то один бит будет установлен в 1. Например: (0) 0000 0000 | (2 5 ) 0010 0000 = 0010 0000 25 (32) 0010 0000 | (2 7 ) 1000 0000 = 1010 0000 При переборе первый элемент массива имеет индекс 0, но соответствующий ему бит в mask должен стоять впереди остальных. Если известно общее количество элементов массива (N), то можно определить степень двойки по формуле N - i - 1 . Действительно, имея третий положительный элемент массива из 10 элементов, следует установить в единицу восьмой с конца бит, а это значит надо использовать вторым операндом битового ИЛИ 2 7 , а 7 как раз будет 10(N) - 2(i) - 1. Другая проблема — как в языке C возвести число в степень. Понятно, что можно написать свой код, но скорее всего в стандартной библиотеке уже есть подобная функция. С помощью заголовочного файла math.h можно подключить библиотеку с математическими функциями. Среди них есть функция pow() , которая принимает два числа и возвращает результат возведения первого числа в степень, выраженную вторым числом. Однако результат возвращается в виде вещественного числа, а нам требуется целое. Как быть? В языке программирования С есть операции приведения типов, которые меняют тип значения с одного на другой. Например, чтобы преобразовать значение вещественной переменной a в целое, следует написать (int) a Вот как может выглядеть вышеописанная программа: #include #include #define N 12 main () { int nums[N] = {7, 3, 9, -5, -3, 2, 1, 0, 16, -4, 2, 0}; int mask = 0, i; for (i=0; i < N; i++) if (nums[i] >= 0) mask = mask | (int)pow(2,N-i-1); printf("%o\n", mask); } Задание Напишите предыдущую программу. Оцените как она работает 1 . Подумайте над тем, как вывести на экран двоичное представление восьмеричного числа. Попробуйте реализовать это. 1 Если у вас не получается скомпилировать программу, добавьте в конце вызова gcc опцию -lm (например, gcc -o bits bits.c -lm). 26 Урок 6. Посимвольный ввод и вывод. Понятие буфера putchar() и getchar() В заголовочном файле stdio.h содержится объявление не только функции printf() , но и многих других, связанных с вводом-выводом. Среди них есть функции, которые обрабатывают по одному символу за вызов — putchar() и getchar() Функция putchar() обычно принимает в качестве аргумента символ, либо символьную переменную и в результате своей работы выводит соответствующий символ на экран. Однако этой функции можно передать любое целое число, но, понятное дело, символа на экране вы можете не получить, если числу не соответствует ни один символ по таблице ASCII. Например: char ch = 'c'; putchar('a'); putchar(98); putchar('\n'); putchar(ch); Результат: ab c Функции putchar() и printf() в определенном смысле взаимозаменяемы, т.к., используя ту или другую, можно получить один и тот же результат. Хотя программный код будет выглядеть по-разному: char str[] = "Hello"; int i; printf("%s\n", str); // первое Hello for (i = 0; str[i] != '\0'; i++) // второе Hello putchar(str[i]); printf("\n"); В результате выполнения этого кода на экране будут напечатаны два слова "Hello", разделенные переходом на новую строку. С putchar() это выглядит несколько сложнее. Как мы знаем, любая строка оканчивается нулевым по таблице ASCII символом, в данном случае этот символ служит сигналом для прекращения вывода на экран. Но если бы понадобилось вывести на экран строку, разделяя ее символы каким-нибудь другим символом (например, тире), то и в случае с printf() было бы не так все просто: char str[] = "Hello"; int i; for (i = 0; str[i] != '\0'; i++) printf("%c-",str[i]); 27 printf("%c%c %c",'\b', '\0', '\n'); for (i = 0; str[i] != '\0'; i++) { putchar(str[i]); putchar('-'); } printf("%c%c %c",'\b', '\0', '\n'); Результат: H-e-l-l-o H-e-l-l-o Поэтому выбор в пользу той или иной функции зависит от ситуации и ваших предпочтений. В отличие от функции putchar() функция getchar() не имеет параметром. Когда getchar() выполняется, она считывает их потока ввода один символ и возвращает его в программу. Полученный таким образом символ может быть присвоен переменной, участвовать в выражениях или выводиться на экран с помощью функций вывода. int a; a = getchar(); printf("%c ", a); putchar(a); putchar('\n'); Если при выполнении этого кода ввести символ, то после нажатия Enter вы увидите два таких же символа на экране: u u u Первый — результат выполнения функции printf() , второй — putchar() . Если вы перед нажатием Enter введете несколько символов, то прочитан будет только первый, остальные будут проигнорированы. Посмотрите вот на этот код: char a, b, c; a = getchar(); putchar(a); b = getchar(); putchar(b); c = getchar(); putchar(c); printf("\n"); Как вы думает, как он будет выполняться? По идее после ввода символа, он должен сразу отображаться на экране функцией putchar() и запрашиваться следующий символ, потому что далее идет снова вызов getchar() . Если вы как корректный пользователь программы 28 сначала введете первый символ и нажмете Enter, то символ отобразиться на экране. Потом вы введете второй символ и после Enter он тоже отобразиться. И тут программа завершится, не дав ввести вам третий символ. Задание Удостоверьтесь в этом сами. Прежде чем попытаться найти объяснение, изобразим "некорректного пользователя" и перед первым нажатием Enter введем несколько символов (больше двух). После Enter вы увидите три первых символа введенной вами строки, и программа завершиться. Хотя куда логичней было бы ожидать, что будет прочитан только первый символ, потом выведен на экран и потом запрошен следующий символ. Задание Проверьте это. Такое странное на первый взгляд поведение программы связано не с языком |