© Copyright Андрей Богатырев. 1992-95
 Email: abs@decart.msu.su
 Txt version is located at

А. Богатырев, 1992-95 - 1 - Си в UNIX Ум подобен желудку. Важно не то, сколько ты в него вложишь, а то, сколько он сможет переварить. В этой книге вы найдете ряд задач, примеров, алгоритмов, советов и стилистичес- ких замечаний по использованию языка программирования "C" (Си) в среде операционной системы UNIX. Здесь собраны этюды разной сложности и "штрихи к портрету" языка Си. Также описаны различные "подводные камни" на которых нередко терпят крушение новички в Си. В этом смысле эту книгу можно местами назвать "Как не надо программировать на Си". В большинстве случаев в качестве платформы используется персональный компьютер IBM PC с какой-либо системой UNIX, либо SPARCstation 20 с системой Solaris 2 (тоже UNIX svr4), но многие примеры без каких-либо изменений (либо с минимумом таковых) могут быть перенесены в среду MS DOS|=, либо на другой тип машины с системой UNIX. Это ваша ВТОРАЯ книга по Си. Эта книга не учебник, а хрестоматия к учебнику. Она не является ни систематическим курсом по Си, ни справочником по нему, и предназ- начена не для одноразового последовательного прочтения, а для чтения в несколько про- ходов на разных этапах вашей "зрелости". Поэтому читать ее следует вместе с "настоя- щим" учебником по Си, среди которых наиболее известна книга Кернигана и Ритчи. Эта книга - не ПОСЛЕДНЯЯ ваша книга по Си. Во-первых потому, что кое-что в языке все же меняется со временем, хотя и настал час, когда стандарт на язык Си наконец принят... Но появился язык C++, который развивается довольно динамично. Еще есть Objective-C. Во-вторых потому, что есть библиотеки и системные вызовы, которые раз- виваются вслед за развитием UNIX и других операционных систем. Следующими вашими (настольными) книгами должны стать "Справочное руководство": man2 (по системным вызо- вам), man3 (по библиотечным функциям). Мощь языка Си - в существующем многообразии библиотек. Прошу вас с первых же шагов следить за стилем оформления своих программ. Делайте отступы, пишите комментарии, используйте осмысленные имена переменных и функций, отделяйте логические части программы друг от друга пустыми строками. Помните, что "лишние" пробелы и пустые строки в Си допустимы везде, кроме изображений констант и имен. Программы на Си, набитые в одну колонку (как на FORTRAN-e) очень тяжело читать и понимать. Из-за этого бывает трудно находить потерянные скобки { и }, потерянные символы `;' и другие ошибки. Существует несколько "школ" оформления программ - приглядитесь к примерам в этой книге и в других источниках - и выберите любую! Ничего страшного, если вы будете смешивать эти стили. Но - ПОДАЛЬШЕ ОТ FORTRAN-а !!! Программу можно автоматически сформатировать к "каноническому" виду при помощи, например, программы cb. cb < НашФайл.c > /tmp/$$ mv /tmp/$$ НашФайл.c но лучше сразу оформлять программу правильно. Выделяйте логически самостоятельные ("замкнутые") части программы в функции (даже если они будут вызываться единственный раз). Функции - не просто средство избежать повторения одних и тех же операторов в тексте программы, но и средство структурирования процесса программирования, делающее программу более понятной. Во- первых, вы можете в другой программе использовать текст уже написанной вами ранее функции вместо того, чтобы писать ее заново. Во-вторых, операцию, оформленную в виде функции, можно рассматривать как неделимый примитив (от довольно простого по смыслу, вроде strcmp, strcpy, до довольно сложного - qsort, malloc, gets) и забыть о его внутреннем устройстве (это хорошо - надо меньше помнить). ____________________ |= MS DOS - торговый знак фирмы Microsoft Corporation. (читается "Майкрософт"); DOS - дисковая операционная система. А. Богатырев, 1992-95 - 2 - Си в UNIX Не гонитесь за краткостью в ущерб ясности. Си позволяет порой писать такие выра- жения, над которыми можно полчаса ломать голову. Если же их записать менее мудрено, но чуть длиннее - они самоочевидны (и этим более защищены от ошибок). В системе UNIX вы можете посмотреть описание любой команды системы или функции Си, набрав команду man названиеФункции (man - от слова manual, "руководство"). Еще одно напутствие: учите английский язык! Практически все языки программирова- ния используют английские слова (в качестве ключевых слов, терминов, имен переменных и функций). Поэтому лучше понимать значение этих слов (хотя и восприятие их как просто неких символов тоже имеет определенные достоинства). Обратно - программирова- ние на Си поможет вам выучить английский. По различным причинам на территории России сейчас используется много разных восьмибитных русских кодировок. Среди них: КОИ-8 Исторически принятая на русских UNIX системах - самая ранняя из появившихся. Отличается тем свойством, что если у нее обрезан восьмой бит: c & 0177 - то она все же читаема с терминала как транслитерация латинских букв. Именно этой коди- ровкой пользуется автор этой книги (как и большинство UNIX-sites сети RelCom). ISO 8859/5 Это американский стандарт на русскую кодировку. А русские программисты к ее разработке не имеют никакого отношения. Ею пользуется большинство коммерческих баз данных. Microsoft 1251 Это та кодировка, которой пользуется Microsoft Windows. Возможно, что именно к этой кодировке придут и UNIX системы (гипотеза 1994 года). Альтернативная кодировка для MS DOS Русская кодировка с псевдографикой, использовавшаяся в MS DOS. Кодировка для Macintosh Это великое "разнообразие" причиняет массу неудобств. Но, господа, это Россия - что значит - широта души и абсолютный бардак. Relax and enjoy. Многие примеры в данной книге даны вместе с ответами - как образцами для подра- жания. Однако мы надеемся, что Вы удержитесь от искушения и сначала проверите свои силы, а лишь потом посмотрите в ответ! Итак, читая примеры - делайте по аналогии. А. Богатырев, 1992-95 - 3 - Си в UNIX 1.1. Составьте программу приветствия с использованием функции printf. По традиции принято печатать фразу "Hello, world !" ("Здравствуй, мир !"). 1.2. Найдите ошибку в программе #include <stdio.h> main(){ printf("Hello, world\n"); } Ответ: раз не объявлено иначе, функция main считается возвращающей целое значение (int). Но функция main не возвращает ничего - в ней просто нет оператора return. Корректно было бы так: #include <stdio.h> main(){ printf("Hello, world\n"); return 0; } или #include <stdio.h> void main(){ printf("Hello, world\n"); exit(0); } а уж совсем корректно - так: #include <stdio.h> int main(int argc, char *argv[]){ printf("Hello, world\n"); return 0; } 1.3. Найдите ошибки в программе #include studio.h main { int i i := 43 print ('В году i недель') } 1.4. Что будет напечатано в приведенном примере, который является частью полной программы: int n; n = 2; printf ("%d + %d = %d\n", n, n, n + n); 1.5. В чем состоят ошибки? А. Богатырев, 1992-95 - 4 - Си в UNIX if( x > 2 ) then x = 2; if x < 1 x = 1; Ответ: в Си нет ключевого слова then, условия в операторах if, while должны браться в ()-скобки. 1.6. Напишите программу, печатающую ваше имя, место работы и адрес. В первом вари- анте программы используйте библиотечную функцию printf, а во втором - puts. 1.7. Составьте программу с использованием следующих постфиксных и префиксных опера- ций: a = b = 5 a + b a++ + b ++a + b --a + b a-- + b Распечатайте полученные значения и проанализируйте результат. 1.8. Цикл for ________________________________________________________________________________ for(INIT; CONDITION; INCR) BODY ________________________________________________________________________________ INIT; repeat: if(CONDITION){ BODY; cont: INCR; goto repeat; } out: ; Цикл while ________________________________________________________________________________ while(COND) BODY ________________________________________________________________________________ cont: repeat: if(CONDITION){ BODY; goto repeat; } out: ; А. Богатырев, 1992-95 - 5 - Си в UNIX Цикл do ________________________________________________________________________________ do BODY while(CONDITION) ________________________________________________________________________________ cont: repeat: BODY; if(CONDITION) goto repeat; out: ; В операторах цикла внутри тела цикла BODY могут присутствовать операторы break и continue; которые означают на наших схемах следующее: #define break goto out #define continue goto cont 1.9. Составьте программу печати прямоугольного треугольника из звездочек * ** *** **** ***** используя цикл for. Введите переменную, значением которой является размер катета тре- угольника. 1.10. Напишите операторы Си, которые выдают строку длины WIDTH, в которой сначала содержится x0 символов '-', затем w символов '*', и до конца строки - вновь символы '-'. Ответ: int x; for(x=0; x < x0; ++x) putchar('-'); for( ; x < x0 + w; x++) putchar('*'); for( ; x < WIDTH ; ++x) putchar('-'); putchar('\n'); либо for(x=0; x < WIDTH; x++) putchar( x < x0 ? '-' : x < x0 + w ? '*' : '-' ); putchar('\n'); 1.11. Напишите программу с циклами, которая рисует треугольник: * *** ***** ******* ********* А. Богатырев, 1992-95 - 6 - Си в UNIX Ответ: /* Треугольник из звездочек */ #include <stdio.h> /* Печать n символов c */ printn(c, n){ while( --n >= 0 ) putchar(c); } int lines = 10; /* число строк треугольника */ void main(argc, argv) char *argv[]; { register int nline; /* номер строки */ register int naster; /* количество звездочек в строке */ register int i; if( argc > 1 ) lines = atoi( argv[1] ); for( nline=0; nline < lines ; nline++ ){ naster = 1 + 2 * nline; /* лидирующие пробелы */ printn(' ', lines-1 - nline); /* звездочки */ printn('*', naster); /* перевод строки */ putchar( '\n' ); } exit(0); /* завершение программы */ } 1.12. В чем состоит ошибка? main(){ /* печать фразы 10 раз */ int i; while(i < 10){ printf("%d-ый раз\n", i+1); i++; } } Ответ: автоматическая переменная i не была проинициализирована и содержит не 0, а какое-то произвольное значение. Цикл может выполниться не 10, а любое число раз (в том числе и 0 по случайности). Не забывайте инициализировать переменные, возьмите описание с инициализацией за правило! int i = 0; Если бы переменная i была статической, она бы имела начальное значение 0. В данном примере было бы еще лучше использовать цикл for, в котором все операции над индексом цикла собраны в одном месте - в заголовке цикла: for(i=0; i < 10; i++) printf(...); А. Богатырев, 1992-95 - 7 - Си в UNIX 1.13. Вспомогательные переменные, не несущие смысловой нагрузки (вроде счетчика пов- торений цикла, не используемого в самом теле цикла) принято по традиции обозначать однобуквенными именами, вроде i, j. Более того, возможны даже такие курьезы: main(){ int _ ; for( _ = 0; _ < 10; _++) printf("%d\n", _ ); } основанные на том, что подчерк в идентификаторах - полноправная буква. 1.14. Найдите 2 ошибки в программе: main(){ int x = 12; printf( "x=%d\n" ); int y; y = 2 * x; printf( "y=%d\n", y ); } Комментарий: в теле функции все описания должны идти перед всеми выполняемыми опера- торами (кроме операторов, входящих в состав описаний с инициализацией). Очень часто после внесения правок в программу некоторые описания оказываются после выполняемых операторов. Именно поэтому рекомендуется отделять строки описания переменных от выполняемых операторов пустыми строками (в этой книге это часто не делается для эко- номии места). 1.15. Найдите ошибку: int n; n = 12; main(){ int y; y = n+2; printf( "%d\n", y ); } Ответ: выполняемый оператор n=12 находится вне тела какой-либо функции. Следует внести его в main() после описания переменной y, либо переписать объявление перед main() в виде int n = 12; В последнем случае присваивание переменной n значения 12 выполнит компилятор еще во время компиляции программы, а не сама программа при своем запуске. Точно так же про- исходит со всеми статическими данными (описанными как static, либо расположенными вне всех функций); причем если их начальное значение не указано явно - то подразумевается 0 ('\0', NULL, ""). Однако нулевые значения не хранятся в скомпилированном выполняе- мом файле, а требуемая "чистая" память расписывается при старте программы. 1.16. По поводу описания переменной с инициализацией: TYPE x = выражение; является (почти) эквивалентом для TYPE x; /* описание */ x = выражение; /* вычисление начального значения */ А. Богатырев, 1992-95 - 8 - Си в UNIX Рассмотрим пример: #include <stdio.h> extern double sqrt(); /* квадратный корень */ double x = 1.17; double s12 = sqrt(12.0); /* #1 */ double y = x * 2.0; /* #2 */ FILE *fp = fopen("out.out", "w"); /* #3 */ main(){ double ss = sqrt(25.0) + x; /* #4 */ ... } Строки с метками #1, #2 и #3 ошибочны. Почему? Ответ: при инициализации статических данных (а s12, y и fp таковыми и являются, так как описаны вне какой-либо функции) выражение должно содержать только константы, поскольку оно вычисляется КОМПИЛЯТОРОМ. Поэтому ни использование значений переменных, ни вызовы функций здесь недопустимы (но можно брать адреса от переменных). В строке #4 мы инициализируем автоматическую переменную ss, т.е. она отводится уже во время выполнения программы. Поэтому выражение для инициализации вычисляется уже не компилятором, а самой программой, что дает нам право использовать переменные, вызовы функций и.т.п., то есть выражения языка Си без ограничений. 1.17. Напишите программу, реализующую эхо-печать вводимых символов. Программа должна завершать работу при получении признака EOF. В UNIX при вводе с клавиатуры признак EOF обычно обозначается одновременным нажатием клавиш CTRL и D (CTRL чуть раньше), что в дальнейшем будет обозначаться CTRL/D; а в MS DOS - клавиш CTRL/Z. Используйте getchar() для ввода буквы и putchar() для вывода. 1.18. Напишите программу, подсчитывающую число символов поступающих со стандартного ввода. Какие достоинства и недостатки у следующей реализации: #include <stdio.h> main(){ double cnt = 0.0; while (getchar() != EOF) ++cnt; printf("%.0f\n", cnt ); } Ответ: и достоинство и недостаток в том, что счетчик имеет тип double. Достоинство - можно подсчитать очень большое число символов; недостаток - операции с double обычно выполняются гораздо медленнее, чем с int и long (до десяти раз), программа будет работать дольше. В повседневных задачах вам вряд ли понадобится иметь счетчик, отличный от long cnt; (печатать его надо по формату "%ld"). 1.19. Составьте программу перекодировки вводимых символов со стандартного ввода по следующему правилу: a -> b b -> c c -> d ... z -> a другой символ -> * Коды строчных латинских букв расположены подряд по возрастанию. 1.20. Составьте программу перекодировки вводимых символов со стандартного ввода по следующему правилу: А. Богатырев, 1992-95 - 9 - Си в UNIX B -> A C -> B ... Z -> Y другой символ -> * Коды прописных латинских букв также расположены по возрастанию. 1.21. Напишите программу, печатающую номер и код введенного символа в восьмеричном и шестнадцатеричном виде. Заметьте, что если вы наберете на вводе строку символов и нажмете клавишу ENTER, то программа напечатает вам на один символ больше, чем вы наб- рали. Дело в том, что код клавиши ENTER, завершившей ввод строки - символ '\n' - тоже попадает в вашу программу (на экране он отображается как перевод курсора в начало следующей строки!). 1.22. Разберитесь, в чем состоит разница между символами '0' (цифра нуль) и '\0' (нулевой байт). Напечатайте printf( "%d %d %c\n", '\0', '0', '0' ); Поставьте опыт: что печатает программа? main(){ int c = 060; /* код символа '0' */ printf( "%c %d %o\n", c, c, c); } Почему печатается 0 48 60? Теперь напишите вместо int c = 060; строчку char c = '0'; 1.23. Что напечатает программа? #include <stdio.h> void main(){ printf("ab\0cd\nxyz"); putchar('\n'); } Запомните, что '\0' служит признаком конца строки в памяти, а '\n' - в файле. Что в строке "abcd\n" на конце неявно уже расположен нулевой байт: 'a','b','c','d','\n','\0' Что строка "ab\0cd\nxyz" - это 'a','b','\0','c','d','\n','x','y',z','\0' Что строка "abcd\0" - избыточна, поскольку будет иметь на конце два нулевых байта (что не вредно, но зачем?). Что printf печатает строку до нулевого байта, а не до закрывающей кавычки. Программа эта напечатает ab и перевод строки. Вопрос: чему равен sizeof("ab\0cd\nxyz")? Ответ: 10. 1.24. Напишите программу, печатающую целые числа от 0 до 100. 1.25. Напишите программу, печатающую квадраты и кубы целых чисел. А. Богатырев, 1992-95 - 10 - Си в UNIX 1.26. Напишите программу, печатающую сумму квадратов первых n целых чисел. 1.27. Напишите программу, которая переводит секунды в дни, часы, минуты и секунды. 1.28. Напишите программу, переводящую скорость из километров в час в метры в секун- дах. 1.29. Напишите программу, шифрующую текст файла путем замены значения символа (нап- ример, значение символа C заменяется на C+1 или на ~C ). 1.30. Напишите программу, которая при введении с клавиатуры буквы печатает на терми- нале ключевое слово, начинающееся с данной буквы. Например, при введении буквы 'b' печатает "break". 1.31. Напишите программу, отгадывающую задуманное вами число в пределах от 1 до 200, пользуясь подсказкой с клавиатуры "=" (равно), "<" (меньше) и ">" (больше). Для уга- дывания числа используйте метод деления пополам. 1.32. Напишите программу, печатающую степени двойки 1, 2, 4, 8, ... Заметьте, что, начиная с некоторого n, результат становится отрицательным из-за пере- полнения целого. 1.33. Напишите подпрограмму вычисления квадратного корня с использованием метода касательных (Ньютона): x(0) = a 1 a x(n+1) = - * ( ---- + x(n)) 2 x(n) Итерировать, пока не будет | x(n+1) - x(n) | < 0.001 Внимание! В данной задаче массив не нужен. Достаточно хранить текущее и предыду- щее значения x и обновлять их после каждой итерации. 1.34. Напишите программу, распечатывающую простые числа до 1000. 1, 2, 3, 5, 7, 11, 13, 17, ... А. Богатырев, 1992-95 - 11 - Си в UNIX /*#!/bin/cc primes.c -o primes -lm * Простые числа. */ #include <stdio.h> #include <math.h> int debug = 0; /* Корень квадратный из числа по методу Ньютона */ #define eps 0.0001 double sqrt (x) double x; { double sq, sqold, EPS; if (x < 0.0) return -1.0; if (x == 0.0) return 0.0; /* может привести к делению на 0 */ EPS = x * eps; sq = x; sqold = x + 30.0; /* != sq */ while (fabs (sq * sq - x) >= EPS) { /* fabs( sq - sqold )>= EPS */ sqold = sq; sq = 0.5 * (sq + x / sq); } return sq; } /* таблица прoстых чисел */ int is_prime (t) register int t; { register int i, up; int not_div; if (t == 2 || t == 3 || t == 5 || t == 7) return 1; /* prime */ if (t % 2 == 0 || t == 1) return 0; /* composite */ up = ceil (sqrt ((double) t)) + 1; i = 3; not_div = 1; while (i <= up && not_div) { if (t % i == 0) { if (debug) fprintf (stderr, "%d поделилось на %d\n", t, i); not_div = 0; break; } i += 2; /* * Нет смысла проверять четные, * потому что если делится на 2*n, * то делится и на 2, * а этот случай уже обработан выше. */ } return not_div; } А. Богатырев, 1992-95 - 12 - Си в UNIX #define COL 6 int n; main (argc, argv) char **argv; { int i, j; int n; if( argc < 2 ){ fprintf( stderr, "Вызов: %s число [-]\n", argv[0] ); exit(1); } i = atoi (argv[1]); /* строка -> целое, ею изображаемое */ if( argc > 2 ) debug = 1; printf ("\t*** Таблица простых чисел от 2 до %d ***\n", i); n = 0; for (j = 1; j <= i; j++) if (is_prime (j)){ /* распечатка в COL колонок */ printf ("%3d%s", j, n == COL-1 ? "\n" : "\t"); if( n == COL-1 ) n = 0; else n++; } printf( "\n---\n" ); exit (0); } 1.35. Составьте программу ввода двух комплексных чисел в виде A + B * I (каждое на отдельной строке) и печати их произведения в том же виде. Используйте scanf и printf. Перед тем, как использовать scanf, проверьте себя: что неверно в нижеприведенном опе- раторе? int x; scanf( "%d", x ); Ответ: должно быть написано "АДРЕС от x", то есть scanf( "%d", &x ); 1.36. Напишите подпрограмму вычисления корня уравнения f(x)=0 методом деления отрезка пополам. Приведем реализацию этого алгоритма для поиска целочисленного квад- ратного корня из целого числа (этот алгоритм может использоваться, например, в машин- ной графике при рисовании дуг): /* Максимальное unsigned long число */ #define MAXINT (~0L) /* Определим имя-синоним для типа unsigned long */ typedef unsigned long ulong; /* Функция, корень которой мы ищем: */ #define FUNC(x, arg) ((x) * (x) - (arg)) /* тогда x*x - arg = 0 означает x*x = arg, то есть * x = корень_квадратный(arg) */ /* Начальный интервал. Должен выбираться исходя из * особенностей функции FUNC */ #define LEFT_X(arg) 0 #define RIGHT_X(arg) (arg > MAXINT)? MAXINT : (arg/2)+1; /* КОРЕНЬ КВАДРАТНЫЙ, округленный вниз до целого. * Решается по методу деления отрезка пополам: * FUNC(x, arg) = 0; x = ? А. Богатырев, 1992-95 - 13 - Си в UNIX */ ulong i_sqrt( ulong arg ) { register ulong mid, /* середина интервала */ rgt, /* правый край интервала */ lft; /* левый край интервала */ lft = LEFT_X(arg); rgt = RIGHT_X(arg); do{ mid = (lft + rgt + 1 )/2; /* +1 для ошибок округления при целочисленном делении */ if( FUNC(mid, arg) > 0 ){ if( rgt == mid ) mid--; rgt = mid ; /* приблизить правый край */ } else lft = mid ; /* приблизить левый край */ } while( lft < rgt ); return mid; } void main(){ ulong i; for(i=0; i <= 100; i++) printf("%ld -> %lu\n", i, i_sqrt(i)); } Использованное нами при объявлении переменных ключевое слово register означает, что переменная является ЧАСТО ИСПОЛЬЗУЕМОЙ, и компилятор должен попытаться разместить ее на регистре процессора, а не в стеке (за счет чего увеличится скорость обращения к этой переменной). Это слово используется как register тип переменная; register переменная; /* подразумевается тип int */ От регистровых переменных нельзя брать адрес: &переменная ошибочно. 1.37. Напишите программу, вычисляющую числа треугольника Паскаля и печатающую их в виде треугольника. C(0,n) = C(n,n) = 1 n = 0... C(k,n+1) = C(k-1,n) + C(k,n) k = 1..n n - номер строки В разных вариантах используйте циклы, рекурсию. 1.38. Напишите функцию вычисления определенного интеграла методом Монте-Карло. Для этого вам придется написать генератор случайных чисел. Си предоставляет стандартный датчик ЦЕЛЫХ равномерно распределенных псевдослучайных чисел: если вы хотите получить целое число из интервала [A..B], используйте int x = A + rand() % (B+1-A); Чтобы получать разные последовательности следует задавать некий начальный параметр последовательности (это называется "рандомизация") при помощи srand( число ); /* лучше нечетное */ Чтобы повторить одну и ту же последовательность случайных чисел несколько раз, вы должны поступать так: srand(NBEG); x=rand(); ... ; x=rand(); /* и повторить все сначала */ srand(NBEG); x=rand(); ... ; x=rand(); Используемый метод получения случайных чисел таков: А. Богатырев, 1992-95 - 14 - Си в UNIX static unsigned long int next = 1L; int rand(){ next = next * 1103515245 + 12345; return ((unsigned int)(next/65536) % 32768); } void srand(seed) unsigned int seed; { next = seed; } Для рандомизации часто пользуются таким приемом: char t[sizeof(long)]; time(t); srand(t[0] + t[1] + t[2] + t[3] + getpid()); 1.39. Напишите функцию вычисления определенного интеграла по методу Симпсона. /*#!/bin/cc $* -lm * Вычисление интеграла по методу Симпсона */ #include <math.h> extern double integral(), sin(), fabs(); #define PI 3.141593 double myf(x) double x; { return sin(x / 2.0); } int niter; /* номер итерации */ void main(){ double integral(); printf("%g\n", integral(0.0, PI, myf, 0.000000001)); /* Заметьте, что myf, а не myf(). * Точное значение интеграла равно 2.0 */ printf("%d итераций\n", niter ); } А. Богатырев, 1992-95 - 15 - Си в UNIX double integral(a, b, f, eps) double a, b; /* концы отрезка */ double eps; /* требуемая точность */ double (*f)(); /* подынтегральная функция */ { register long i; double fab = (*f)(a) + (*f)(b); /* сумма на краях */ double h, h2; /* шаг и удвоенный шаг */ long n, n2; /* число точек разбиения и оно же удвоенное */ double Sodd, Seven; /* сумма значений f в нечетных и в четных точках */ double S, Sprev;/* значение интеграла на данной и на предыдущей итерациях */ double x; /* текущая абсцисса */ niter = 0; n = 10L; /* четное число */ n2 = n * 2; h = fabs(b - a) / n2; h2 = h * 2.0; /* Вычисляем первое приближение */ /* Сумма по нечетным точкам: */ for( Sodd = 0.0, x = a+h, i = 0; i < n; i++, x += h2 ) Sodd += (*f)(x); /* Сумма по четным точкам: */ for( Seven = 0.0, x = a+h2, i = 0; i < n-1; i++, x += h2 ) Seven += f(x); /* Предварительное значение интеграла: */ S = h / 3.0 * (fab + 4.0 * Sodd + 2.0 * Seven ); do{ niter++; Sprev = S; /* Вычисляем интеграл с половинным шагом */ h2 = h; h /= 2.0; if( h == 0.0 ) break; /* потеря значимости */ n = n2; n2 *= 2; Seven = Seven + Sodd; /* Вычисляем сумму по новым точкам: */ for( Sodd = 0.0, x = a+h, i = 0; i < n; i++, x += h2 ) Sodd += (*f)(x); /* Значение интеграла */ S = h / 3.0 * (fab + 4.0 * Sodd + 2.0 * Seven ); } while( niter < 31 && fabs(S - Sprev) / 15.0 >= eps ); /* Используем условие Рунге для окончания итераций */ return ( 16.0 * S - Sprev ) / 15.0 ; /* Возвращаем уточненное по Ричардсону значение */ } А. Богатырев, 1992-95 - 16 - Си в UNIX 1.40. Где ошибка? struct time_now{ int hour, min, sec; } X = { 13, 08, 00 }; /* 13 часов 08 минут 00 сек.*/ Ответ: 08 - восьмеричное число (так как начинается с нуля)! А в восьмеричных числах цифры 8 и 9 не бывают. 1.41. Дан текст: int i = -2; i <<= 2; printf("%d\n", i); /* печать сдвинутого i : -8 */ i >>= 2; printf("%d\n", i); /* печатается -2 */ Закомментируем две строки (исключая их из программы): int i = -2; i <<= 2; /* printf("%d\n", i); /* печать сдвинутого i : -8 */ i >>= 2; */ printf("%d\n", i); /* печатается -2 */ Почему теперь возникает ошибка? Указание: где кончается комментарий? Ответ: Си не допускает вложенных комментариев. Вместо этого часто используются конструкции вроде: #ifdef COMMENT ... закомментированный текст ... #endif /*COMMENT*/ и вроде /**/ printf("here");/* отладочная выдача включена */ /* printf("here");/* отладочная выдача выключена */ или /* выключено(); /**/ включено(); /**/ А вот дешевый способ быстро исключить оператор (с возможностью восстановления) - конец комментария занимает отдельную строку, что позволяет отредактировать такой текст редактором почти не сдвигая курсор: /*printf("here"); */ 1.42. Почему программа печатает неверное значение для i2 ? А. Богатырев, 1992-95 - 17 - Си в UNIX int main(int argc, char *argv[]){ int i1, i2; i1 = 1; /* Инициализируем i1 / i2 = 2; /* Инициализируем i2 */ printf("Numbers %d %d\n", i1, i2); return(0); } Ответ: в первом операторе присваивания не закрыт комментарий - весь второй оператор присваивания полностью проигнорировался! Правильный вариант: int main(int argc, char *argv[]){ int i1, i2; i1 = 1; /* Инициализируем i1 */ i2 = 2; /* Инициализируем i2 */ printf("Numbers %d %d\n", i1, i2); return(0); } 1.43. А вот "шальной" комментарий. void main(){ int n = 10; int *ptr = &n; int x, y = 40; x = y/*ptr /* должно быть 4 */ + 1; printf( "%d\n", x ); /* пять */ exit(0); } /* или такой пример из жизни - взят из переписки в Relcom */ ... cost = nRecords/*pFactor /* divided by Factor, and */ + fixMargin; /* plus the precalculated */ ... Результат непредсказуем. Дело в том, что y/*ptr превратилось в начало комментария! Поэтому бинарные операции принято окружать пробелами. x = y / *ptr /* должно быть 4 */ + 1; 1.44. Найдите ошибки в директивах препроцессора Си |- (вертикальная черта обозначает левый край файла). ____________________ |- Препроцессор Си - это программа /lib/cpp А. Богатырев, 1992-95 - 18 - Си в UNIX | | #include <stdio.h> |#include < sys/types.h > |# define inc (x) ((x) + 1) |#define N 12; |#define X -2 | |... printf( "n=%d\n", N ); |... p = 4-X; Ответ: в первой директиве стоит пробел перед #. Диез должен находиться в первой позиции строки. Во второй директиве в <> находятся лишние пробелы, не относящиеся к имени файла - препроцессор не найдет такого файла! В данном случае "красота" пошла во вред делу. В третьей - между именем макро inc и его аргументом в круглых скобках (x) стоит пробел, который изменяет весь смысл макроопределения: вместо макроса с параметром inc(x) мы получаем, что слово inc будет заменяться на (x)((x)+1). Заметим однако, что пробелы после # перед именем директивы вполне допустимы. В четвертом случае показана характерная опечатка - символ ; после определения. В результате напи- санный printf() заменится на printf( "n=%d\n", 12; ); где лишняя ; даст синтаксическую ошибку. В пятом случае ошибки нет, но нас ожидает неприятность в строке p=4-X; которая расширится в строку p=4--2; являющуюся синтаксически неверной. Чтобы избежать подоб- ной ситуации, следовало бы написать p = 4 - X; /* через пробелы */ но еще проще (и лучше) взять макроопределение в скобки: #define X (-2) 1.45. Напишите функцию max(x, y), возвращающую большее из двух значений. Напишите аналогичное макроопределение. Напишите макроопределения min(x, y) и abs(x) (abs - модуль числа). Ответ: #define abs(x) ((x) < 0 ? -(x) : (x)) #define min(x,y) (((x) < (y)) ? (x) : (y)) Зачем x взят в круглые скобки (x)? Предположим, что мы написали #define abs(x) (x < 0 ? -x : x ) вызываем abs(-z) abs(a|b) получаем (-z < 0 ? --z : -z ) (a|b < 0 ? -a|b : a|b ) У нас появилась "дикая" операция --z; а выражение a|b<0 соответствует a|(b<0), с сов- сем другим порядком операций! Поэтому заключение всех аргументов макроса в его теле в круглые скобки позволяет избежать многих неожиданных проблем. Придерживайтесь этого правила! Вот пример, показывающий зачем полезно брать в скобки все определение: #define div(x, y) (x)/(y) При вызове А. Богатырев, 1992-95 - 19 - Си в UNIX z = sizeof div(1, 2); превратится в z = sizeof(1) / (2); что равно sizeof(int)/2, а не sizeof(int). Вариант #define div(x, y) ((x) / (y)) будет работать правильно. 1.46. Макросы, в отличие от функций, могут порождать непредвиденные побочные эффекты: int sqr(int x){ return x * x; } #define SQR(x) ((x) * (x)) main(){ int y=2, z; z = sqr(y++); printf("y=%d z=%d\n", y, z); y = 2; z = SQR(y++); printf("y=%d z=%d\n", y, z); } Вызов функции sqr печатает "y=3 z=4", как мы и ожидали. Макрос же SQR расширяется в z = ((y++) * (y++)); и результатом будет "y=4 z=6", где z совсем не похоже на квадрат числа 2. 1.47. ANSI препроцессор|- языка Си имеет оператор ## - "склейка лексем": #define VAR(a, b) a ## b #define CV(x) command_ ## x main(){ int VAR(x, 31) = 1; /* превратится в int x31 = 1; */ int CV(a) = 2; /* даст int command_a = 2; */ ... } Старые версии препроцессора не обрабатывают такой оператор, поэтому раньше использо- вался такой трюк: #define VAR(a, b) a/**/b в котором предполагается, что препроцессор удаляет комментарии из текста, не заменяя их на пробелы. Это не всегда так, поэтому такая конструкция не мобильна и пользо- ваться ею не рекомендуется. 1.48. Напишите программу, распечатывающую максимальное и минимальное из ряда чисел, вводимых с клавиатуры. Не храните вводимые числа в массиве, вычисляйте max и min сразу при вводе очередного числа! ____________________ |- ANSI - American National Standards Institute, разработавший стандарт на язык Си и его окружение. А. Богатырев, 1992-95 - 20 - Си в UNIX #include <stdio.h> main(){ int max, min, x, n; for( n=0; scanf("%d", &x) != EOF; n++) if( n == 0 ) min = max = x; else{ if( x > max ) max = x; if( x < min ) min = x; } printf( "Ввели %d чисел: min=%d max=%d\n", n, min, max); } Напишите аналогичную программу для поиска максимума и минимума среди элементов мас- сива, изначально min=max=array[0]; 1.49. Напишите программу, которая сортирует массив заданных чисел по возрастанию (убыванию) методом пузырьковой сортировки. Когда вы станете более опытны в Си, напи- шите сортировку методом Шелла. /* * Сортировка по методу Шелла. * Сортировке подвергается массив указателей на данные типа obj. * v------.-------.------.-------.------0 * ! ! ! ! * * * * * * элементы типа obj * Программа взята из книги Кернигана и Ритчи. */ #include <stdio.h> #include <string.h> #include <locale.h> #define obj char static shsort (v,n,compare) int n; /* длина массива */ obj *v[]; /* массив указателей */ int (*compare)(); /* функция сравнения соседних элементов */ { int g, /* расстояние, на котором происходит сравнение */ i,j; /* индексы сравниваемых элементов */ obj *temp; for( g = n/2 ; g > 0 ; g /= 2 ) for( i = g ; i < n ; i++ ) for( j = i-g ; j >= 0 ; j -= g ) { if((*compare)(v[j],v[j+g]) <= 0) break; /* уже в правильном порядке */ /* обменять указатели */ temp = v[j]; v[j] = v[j+g]; v[j+g] = temp; /* В качестве упражнения можете написать * при помощи curses-а программу, * визуализирующую процесс сортировки: * например, изображающую эту перестановку * элементов массива */ } } А. Богатырев, 1992-95 - 21 - Си в UNIX /* сортировка строк */ ssort(v) obj **v; { extern less(); /* функция сравнения строк */ int len; /* подсчет числа строк */ len=0; while(v[len]) len++; shsort(v,len,less); } /* Функция сравнения строк. * Вернуть целое меньше нуля, если a < b * ноль, если a == b * больше нуля, если a > b */ less(a,b) obj *a,*b; { return strcoll(a,b); /* strcoll - аналог strcmp, * но с учетом алфавитного порядка букв. */ } char *strings[] = { "Яша", "Федя", "Коля", "Гриша", "Сережа", "Миша", "Андрей Иванович", "Васька", NULL }; int main(){ char **next; setlocale(LC_ALL, ""); ssort( strings ); /* распечатка */ for( next = strings ; *next ; next++ ) printf( "%s\n", *next ); return 0; } 1.50. Реализуйте алгоритм быстрой сортировки. А. Богатырев, 1992-95 - 22 - Си в UNIX /* Алгоритм быстрой сортировки. Работа алгоритма "анимируется" * (animate-оживлять) при помощи библиотеки curses. * cc -o qsort qsort.c -lcurses -ltermcap */ #include "curses.h" #define N 10 /* длина массива */ /* массив, подлежащий сортировке */ int target [N] = { 7, 6, 10, 4, 2, 9, 3, 8, 5, 1 }; int maxim; /* максимальный элемент массива */ /* quick sort */ qsort (a, from, to) int a[]; /* сортируемый массив */ int from; /* левый начальный индекс */ int to; /* правый конечный индекс */ { register i, j, x, tmp; if( from >= to ) return; /* число элементов <= 1 */ i = from; j = to; x = a[ (i+j) / 2 ]; /* значение из середины */ do{ /* сужение вправо */ while( a[i] < x ) i++ ; /* сужение влево */ while( x < a[j] ) j--; if( i <= j ){ /* обменять */ tmp = a[i]; a[i] = a[j] ; a[j] = tmp; i++; j--; demochanges(); /* визуализация */ } } while( i <= j ); /* Теперь обе части сошлись в одной точке. * Длина левой части = j - from + 1 * правой = to - i + 1 * Все числа в левой части меньше всех чисел в правой. * Теперь надо просто отсортировать каждую часть в отдельности. * Сначала сортируем более короткую (для экономии памяти * в стеке ). Рекурсия: */ if( (j - from) < (to - i) ){ qsort( a, from, j ); qsort( a, i, to ); } else { qsort( a, i, to ); qsort( a, from, j ); } } А. Богатырев, 1992-95 - 23 - Си в UNIX int main (){ register i; initscr(); /* запуск curses-а */ /* поиск максимального числа в массиве */ for( maxim = target[0], i = 1 ; i < N ; i++ ) if( target[i] > maxim ) maxim = target[i]; demochanges(); qsort( target, 0, N-1 ); demochanges(); mvcur( -1, -1, LINES-1, 0); /* курсор в левый нижний угол */ endwin(); /* завершить работу с curses-ом */ return 0; } #define GAPY 2 #define GAPX 20 /* нарисовать картинку */ demochanges(){ register i, j; int h = LINES - 3 * GAPY - N; int height; erase(); /* зачистить окно */ attron( A_REVERSE ); /* рисуем матрицу упорядоченности */ for( i=0 ; i < N ; i++ ) for( j = 0; j < N ; j++ ){ move( GAPY + i , GAPX + j * 2 ); addch( target[i] >= target[j] ? '*' : '.' ); addch( ' ' ); /* Рисовать '*' если элементы * идут в неправильном порядке. * Возможен вариант проверки target[i] > target[j] */ } attroff( A_REVERSE ); /* массив */ for( i = 0 ; i < N ; i++ ){ move( GAPY + i , 5 ); printw( "%4d", target[i] ); height = (long) h * target[i] / maxim ; for( j = 2 * GAPY + N + (h - height) ; j < LINES - GAPY; j++ ){ move( j, GAPX + i * 2 ); addch( '|' ); } } refresh(); /* проявить картинку */ sleep(1); } А. Богатырев, 1992-95 - 24 - Си в UNIX 1.51. Реализуйте приведенный фрагмент программы без использования оператора goto и без меток. if ( i > 10 ) goto M1; goto M2; M1: j = j + i; flag = 2; goto M3; M2: j = j - i; flag = 1; M3: ; Заметьте, что помечать можно только оператор (может быть пустой); поэтому не может встретиться фрагмент { ..... Label: } а только { ..... Label: ; } 1.52. В каком случае оправдано использование оператора goto? Ответ: при выходе из вложенных циклов, т.к. оператор break позволяет выйти только из самого внутреннего цикла (на один уровень). 1.53. К какому if-у относится else? if(...) ... if(...) ... else ... Ответ: ко второму (к ближайшему предшествующему, для которого нет другого else). Вообще же лучше явно расставлять скобки (для ясности): if(...){ ... if(...) ... else ... } if(...){ ... if(...) ... } else ... 1.54. Макроопределение, чье тело представляет собой последовательность операторов в {...} скобках (блок), может вызвать проблемы при использовании его в условном опера- торе if с else-частью: #define MACRO { x=1; y=2; } if(z) MACRO; else .......; Мы получим после макрорасширения if(z) { x=1; y=2; } /* конец if-а */ ; else .......; /* else ни к чему не относится */ то есть синтаксически ошибочный фрагмент, так как должно быть либо if(...) один_оператор; else ..... либо if(...){ последовательность; ...; операторов; } else ..... где точка-с-запятой после } не нужна. С этим явлением борются, оформляя блок {...} в виде do{...}while(0) #define MACRO do{ x=1; y=2; }while(0) Тело такого "цикла" выполняется единственный раз, при этом мы получаем правильный текст: А. Богатырев, 1992-95 - 25 - Си в UNIX if(z) do{ x=1; y=2; }while(0); else .......; 1.55. В чем ошибка (для знающих язык "Паскаль")? int x = 12; if( x < 20 and x > 10 ) printf( "O'K\n"); else if( x > 100 or x < 0 ) printf( "Bad x\n"); else printf( "x=%d\n", x); Напишите #define and && #define or || 1.56. Почему программа зацикливается? Мы хотим подсчитать число пробелов и табуля- ций в начале строки: int i = 0; char *s = " 3 spaces"; while(*s == ' ' || *s++ == '\t') printf( "Пробел %d\n", ++i); Ответ: логические операции || и && выполняются слева направо; как только какое-то условие в || оказывается истинным (а в && ложным) - дальнейшие условия просто не вычисляются. В нашем случае условие *s==' ' сразу же верно, и операция s++ из второго условия не выполняется! Мы должны были написать хотя бы так: while(*s == ' ' || *s == '\t'){ printf( "Пробел %d\n", ++i); s++; } С другой стороны, это свойство || и && черезвычайно полезно, например: if( x != 0.0 && y/x < 1.0 ) ... ; Если бы мы не вставили проверку на 0, мы могли бы получить деление на 0. В данном же случае при x==0 деление просто не будет вычисляться. Вот еще пример: int a[5], i; for(i=0; i < 5 && a[i] != 0; ++i) ...; Если i выйдет за границу массива, то сравнение a[i] с нулем уже не будет вычисляться, т.е. попытки прочесть элемент не входящий в массив не произойдет. Это свойство && позволяет писать довольно неочевидные конструкции, вроде if((cond) && f()); что оказывается эквивалентным if( cond ) f(); Вообще же if(C1 && C2 && C3) DO; эквивалентно if(C1) if(C2) if(C3) DO; и для "или" А. Богатырев, 1992-95 - 26 - Си в UNIX if(C1 || C2 || C3) DO; эквивалентно if(C1) goto ok; else if(C2) goto ok; else if(C3){ ok: DO; } Вот еще пример, пользующийся этим свойством || #include <stdio.h> main(argc, argv) int argc; char *argv[]; { FILE *fp; if(argc < 2 || (fp=fopen(argv[1], "r")) == NULL){ fprintf(stderr, "Плохое имя файла\n"); exit(1); /* завершить программу */ } ... } Если argc==1, то argv[1] не определено, однако в этом случае попытки открыть файл с именем argv[1] просто не будет предпринято! Ниже приведен еще один содержательный пример, представляющий собой одну из воз- можных схем написания "двуязычных" программ, т.е. выдающих сообщения на одном из двух языков по вашему желанию. Проверяется переменная окружения MSG (или LANG): ЯЗЫК: 1) "MSG=engl" английский 2) MSG нет в окружении английский 3) "MSG=rus" русский Про окружение и функцию getenv() смотри в главе "Взаимодействие с UNIX", про strchr() - в главе "Массивы и строки". #include <stdio.h> int _ediag = 0; /* язык диагностик: 1-русский */ extern char *getenv(), *strchr(); #define ediag(e,r) (_ediag?(r):(e)) main(){ char *s; _ediag = ((s=getenv("MSG")) != NULL && strchr("rRрР", *s) != NULL); printf(ediag("%d:english\n", "%d:русский\n"), _ediag); } Если переменная MSG не определена, то s==NULL и функция strchr(s,...) не вызывается (ее первый фргумент не должен быть NULL-ом). Здесь ее можно было бы упрощенно заме- нить на *s=='r'; тогда если s равно NULL, то обращение *s было бы незаконно (обраще- ние по указателю NULL дает непредсказуемые результаты и, скорее всего, вызовет крах программы). 1.57. Иногда логическое условие можно сделать более понятным, используя правила де- Моргана: a && b = ! ( !a || !b ) a || b = ! ( !a && !b ) а также учитывая, что ! !a = a ! (a == b) = (a != b) Например: А. Богатырев, 1992-95 - 27 - Си в UNIX if( c != 'a' && c != 'b' && c != 'c' )...; превращается в if( !(c == 'a' || c == 'b' || c == 'c')) ...; 1.58. Пример, в котором используются побочные эффекты вычисления выражений. Обычно значение выражения присваивается некоторой переменной, но это не необходимо. Поэтому можно использовать свойства вычисления && и || в выражениях (хотя это не есть самый понятный способ написания программ, скорее некоторый род извращения). Ограничение тут таково: все части выражения должны возвращать значения. #include <stdio.h> extern int errno; /* код системной ошибки */ FILE *fp; int openFile(){ errno = 0; fp = fopen("/etc/inittab", "r"); printf("fp=%x\n", fp); return(fp == NULL ? 0 : 1); } int closeFile(){ printf("closeFile\n"); if(fp) fclose(fp); return 0; } int die(int code){ printf("exit(%d)\n", code); exit(code); return 0; } void main(){ char buf[2048]; if( !openFile()) die(errno); closeFile(); openFile() || die(errno); closeFile(); /* если файл открылся, то die() не вычисляется */ openFile() ? 0 : die(errno); closeFile(); if(openFile()) closeFile(); openFile() && closeFile(); /* вычислить closeFile() только если openFile() удалось */ openFile() && (printf("%s", fgets(buf, sizeof buf, fp)), closeFile()); } В последней строке использован оператор "запятая": (a,b,c) возвращает значение выра- жения c. 1.59. Напишите функцию, вычисляющую сумму массива заданных чисел. 1.60. Напишите функцию, вычисляющую среднее значение массива заданных чисел. 1.61. Что будет напечатано в результате работы следующего цикла? for ( i = 36; i > 0; i /= 2 ) printf ( "%d%s", i, i==1 ? ".\n":", "); А. Богатырев, 1992-95 - 28 - Си в UNIX Ответ: 36, 18, 9, 4, 2, 1. 1.62. Найдите ошибки в следующей программе: main { int i, j, k(10); for ( i = 0, i <= 10, i++ ){ k[i] = 2 * i + 3; for ( j = 0, j <= i, j++ ) printf ("%i\n", k[j]); } } Обратите внимание на формат %i, существует ли такой формат? Есть ли это тот формат, по которому следует печатать значения типа int? 1.63. Напишите программу, которая распечатывает элементы массива. Напишите прог- рамму, которая распечатывает элементы массива по 5 чисел в строке. 1.64. Составьте программу считывания строк символов из стандартного ввода и печати номера введенной строки, адреса строки в памяти ЭВМ, значения строки, длины строки. 1.65. Стилистическое замечание: в операторе return возвращаемое выражение не обяза- тельно должно быть в ()-скобках. Дело в том, что return - не функция, а оператор. return выражение; return (выражение); Однако если вы вызываете функцию (например, exit) - то аргументы должны быть в круг- лых скобках: exit(1); но не exit 1; 1.66. Избегайте ситуации, когда функция в разных ветвях вычисления то возвращает некоторое значение, то не возвращает ничего: int func (int x) { if( x > 10 ) return x*2; if( x == 10 ) return (10); /* а здесь - неявный return; без значения */ } при x < 10 функция вернет непредсказуемое значение! Многие компиляторы распознают такие ситуации и выдают предупреждение. 1.67. Напишите программу, запрашивающую ваше имя и "приветствующую" вас. Напишите функцию чтения строки. Используйте getchar() и printf(). Ответ: #include <stdio.h> /* standard input/output */ main(){ char buffer[81]; int i; printf( "Введите ваше имя:" ); while((i = getstr( buffer, sizeof buffer )) != EOF){ printf( "Здравствуй, %s\n", buffer ); printf( "Введите ваше имя:" ); } } getstr( s, maxlen ) char *s; /* куда поместить строку */ int maxlen; /* длина буфера: А. Богатырев, 1992-95 - 29 - Си в UNIX макс. длина строки = maxlen-1 */ { int c; /* не char! (почему ?) */ register int i = 0; maxlen--; /* резервируем байт под конечный '\0' */ while(i < maxlen && (c = getchar()) != '\n' && c != EOF ) s[i++] = c; /* обратите внимание, что сам символ '\n' * в строку не попадет */ s[i] = '\0'; /* признак конца строки */ return (i == 0 && c == EOF) ? EOF : i; /* вернем длину строки */ } Вот еще один вариант функции чтения строки: в нашем примере ее следует вызывать как fgetstr(buffer,sizeof(buffer),stdin); Это подправленный вариант стандартной функции fgets (в ней строки @1 и @2 обменяны местами). char *fgetstr(char *s, int maxlen, register FILE *fp){ register c; register char *cs = s; while(--maxlen > 0 && (c = getc(fp)) != EOF){ if(c == '\n') break; /* @1 */ *cs++ = c; /* @2 */ } if(c == EOF && cs == s) return NULL; /* Заметьте, что при EOF строка s не меняется! */ *cs = '\0'; return s; } Исследуйте поведение этих функций, когда входная строка слишком длинная (длиннее max- len). Замечание: вместо нашей "рукописной" функции getstr() мы могли бы использовать стандартную библиотечную функцию gets(buffer). 1.68. Объясните, почему d стало отрицательным и почему %X печатает больше F, чем в исходном числе? Пример выполнялся на 32-х битной машине. main(){ unsigned short u = 65535; /* 16 бит: 0xFFFF */ short d = u; /* 15 бит + знаковый бит */ printf( "%X %d\n", d, d); /* FFFFFFFF -1 */ } Указание: рассмотрите двоичное представление чисел (смотри приложение). Какие приве- дения типов здесь происходят? 1.69. Почему 128 превратилось в отрицательное число? main() { /*signed*/ char c = 128; /* биты: 10000000 */ unsigned char uc = 128; int d = c; /* используется 32-х битный int */ printf( "%d %d %x\n", c, d, d ); /* -128 -128 ffffff80 */ d = uc; printf( "%d %d %x\n", uc, d, d ); /* 128 128 80 */ } А. Богатырев, 1992-95 - 30 - Си в UNIX Ответ: при приведении char к int расширился знаковый бит (7-ой), заняв всю старшую часть слова. Знаковый бит int-а стал равен 1, что является признаком отрицательного числа. То же будет происходить со всеми значениями c из диапазона 128..255 (содержа- щими бит 0200). При приведении unsigned char к int знаковый бит не расширяется. Можно было поступить еще и так: printf( "%d\n", c & 0377 ); Здесь c приводится к типу int (потому что при использовании в аргументах функции тип char ВСЕГДА приводится к типу int), затем &0377 занулит старший байт полученного целого числа (состоящий из битов 1), снова превратив число в положительное. 1.70. Почему printf("%d\n", '\377' == 0377 ); printf("%d\n", '\xFF' == 0xFF ); печатает 0 (ложь)? Ответ: по той же причине, по которой printf("%d %d\n", '\377', 0377); печатает -1 255, а именно: char '\377' приводится в выражениях к целому расширением знакового бита (а 0377 - уже целое). 1.71. Рассмотрим программу #include <stdio.h> int main(int ac, char **av){ int c; while((c = getchar()) != EOF) switch(c){ case 'ы': printf("Буква ы\n"); break; case 'й': printf("Буква й\n"); break; default: printf("Буква с кодом %d\n", c); break; } return 0; } Она работает так: % a.out йфыв Буква с кодом 202 Буква с кодом 198 Буква с кодом 217 Буква с кодом 215 Буква с кодом 10 ^D % Выполняется всегда default, почему не выполняются case 'ы' и case 'й'? Ответ: русские буквы имеют восьмой бит (левый) равный 1. В case такой байт при- водится к типу int расширением знакового бита. В итоге получается отрицательное число. Пример: void main(void){ int c = 'й'; printf("%d\n", c); } печатает -54 А. Богатырев, 1992-95 - 31 - Си в UNIX Решением служит подавление расширения знакового бита: #include <stdio.h> /* Одно из двух */ #define U(c) ((c) & 0xFF) #define UC(c) ((unsigned char) (c)) int main(int ac, char **av){ int c; while((c = getchar()) != EOF) switch(c){ case U('ы'): printf("Буква ы\n"); break; case UC('й'): printf("Буква й\n"); break; default: printf("Буква с кодом %d\n", c); break; } return 0; } Она работает правильно: % a.out йфыв Буква й Буква с кодом 198 Буква ы Буква с кодом 215 Буква с кодом 10 ^D % Возможно также использование кодов букв: case 0312: но это гораздо менее наглядно. Подавление знакового бита необходимо также и в опера- торах if: int c; ... if(c == 'й') ... следует заменить на if(c == UC('й')) ... Слева здесь - signed int, правую часть компилятор тоже приводит к signed int. Прихо- дится явно говорить, что справа - unsigned. 1.72. Рассмотрим программу, которая должна напечатать числа от 0 до 255. Для этих чисел в качестве счетчика достаточен один байт: int main(int ac, char *av[]){ unsigned char ch; for(ch=0; ch < 256; ch++) printf("%d\n", ch); return 0; } Однако эта программа зацикливается, поскольку в момент, когда ch==255, это значение меньше 256. Следующим шагом выполняется ch++, и ch становится равно 0, ибо для char А. Богатырев, 1992-95 - 32 - Си в UNIX вычисления ведутся по модулю 256 (2 в 8 степени). То есть в данном случае 255+1=0 Решений существует два: первое - превратить unsigned char в int. Второе - вста- вить явную проверку на последнее значение диапазона. int main(int ac, char *av[]){ unsigned char ch; for(ch=0; ; ch++){ printf("%d\n", ch); if(ch == 255) break; } return 0; } 1.73. Подумайте, почему для unsigned a, b, c; a < b + c не эквивалентно a - b < c (первое - более корректно). Намек в виде примера (он выполнялся на 32-битной машине): a = 1; b = 3; c = 2; printf( "%u\n", a - b ); /* 4294967294, хотя в нормальной арифметике 1 - 3 = -2 */ printf( "%d\n", a < b + c ); /* 1 */ printf( "%d\n", a - b < c ); /* 0 */ Могут ли unsigned числа быть отрицательными? 1.74. Дан текст: short x = 40000; printf("%d\n", x); Печатается -25536. Объясните эффект. Указание: каково наибольшее представимое корот- кое целое (16 битное)? Что на самом деле оказалось в x? (лишние слева биты - обруба- ются). 1.75. Почему в примере double x = 5 / 2; printf( "%g\n", x ); значение x равно 2 а не 2.5 ? Ответ: производится целочисленное деление, затем в присваивании целое число 2 приводится к типу double. Чтобы получился ответ 2.5, надо писать одним из следующих способов: double x = 5.0 / 2; x = 5 / 2.0; x = (double) 5 / 2; x = 5 / (double) 2; x = 5.0 / 2.0; то есть в выражении должен быть хоть один операнд типа double. Объясните, почему следующие три оператора выдают такие значения: А. Богатырев, 1992-95 - 33 - Си в UNIX double g = 9.0; int t = 3; double dist = g * t * t / 2; /* 40.5 */ dist = g * (t * t / 2); /* 36.0 */ dist = g * (t * t / 2.0); /* 40.5 */ В каких случаях деление целочисленное, в каких - вещественное? Почему? 1.76. Странслируйте пример на машине с длиной слова int равной 16 бит: long n = 1024 * 1024; long nn = 512 * 512; printf( "%ld %ld\n", n, nn ); Почему печатается 0 0 а не 1048576 262144? Ответ: результат умножения (2**20 и 2**18) - это целое число; однако оно слишком велико для сохранения в 16 битах, поэтому старшие биты обрубаются. Получается 0. Затем в присваивании это уже обрубленное значение приводится к типу long (32 бита) - это все равно будет 0. Чтобы получить корректный результат, надо чтобы выражение справа от = уже имело тип long и сразу сохранялось в 32 битах. Для этого оно должно иметь хоть один операнд типа long: long n = (long) 1024 * 1024; long nn = 512 * 512L; 1.77. Найдите ошибку в операторе: x - = 4; /* вычесть из x число 4 */ Ответ: между `-' и `=' не должно быть пробела. Операция вида x @= expr; означает x = x @ expr; (где @ - одна из операций + - * / % ^ >> << & |), причем x здесь вычисляется единст- венный раз (т.е. такая форма не только короче и понятнее, но и экономичнее). Однако имеется тонкое отличие a=a+n от a+=n; оно заключается в том, сколько раз вычисляется a. В случае a+=n единожды; в случае a=a+n два раза. А. Богатырев, 1992-95 - 34 - Си в UNIX #include <stdio.h> static int x = 0; int *iaddr(char *msg){ printf("iaddr(%s) for x=%d evaluated\n", msg, x); return &x; } int main(){ static int a[4]; int *p, i; printf( "1: "); x = 0; (*iaddr("a"))++; printf( "2: "); x = 0; *iaddr("b") += 1; printf( "3: "); x = 0; *iaddr("c") = *iaddr("d") + 1; for(i=0, p = a; i < sizeof(a)/sizeof(*a); i++) a[i] = 0; *p++ += 1; for(i=0; i < sizeof(a)/sizeof(*a); i++) printf("a[%d]=%d ", i, a[i]); printf("offset=%d\n", p - a); for(i=0, p = a; i < sizeof(a)/sizeof(*a); i++) a[i] = 0; *p++ = *p++ + 1; for(i=0; i < sizeof(a)/sizeof(*a); i++) printf("a[%d]=%d ", i, a[i]); printf("offset=%d\n", p - a); return 0; } Выдача: 1: iaddr(a) for x=0 evaluated 2: iaddr(b) for x=0 evaluated 3: iaddr(d) for x=0 evaluated iaddr(c) for x=0 evaluated a[0]=1 a[1]=0 a[2]=0 a[3]=0 offset=1 a[0]=1 a[1]=0 a[2]=0 a[3]=0 offset=2 Заметьте также, что a[i++] += z; это a[i] = a[i] + z; i++; а вовсе не a[i++] = a[i++] + z; 1.78. Операция y = ++x; эквивалентна y = (x = x+1, x); а операция y = x++; эквивалентна y = (tmp = x, x = x+1, tmp); или y = (x += 1) - 1; где tmp - временная псевдопеременная того же типа, что и x. Операция `,' выдает А. Богатырев, 1992-95 - 35 - Си в UNIX значение последнего выражения из перечисленных (подробнее см. ниже). Пусть x=1. Какие значения будут присвоены x и y после выполнения оператора y = ++x + ++x + ++x; 1.79. Пусть i=4. Какие значения будут присвоены x и i после выполнения оператора x = --i + --i + --i; 1.80. Пусть x=1. Какие значения будут присвоены x и y после выполнения оператора y = x++ + x++ + x++; 1.81. Пусть i=4. Какие значения будут присвоены i и y после выполнения оператора y = i-- + i-- + i--; 1.82. Корректны ли операторы char *p = "Jabberwocky"; char s[] = "0123456789?"; int i = 0; s[i] = p[i++]; или *p = *++p; или s[i] = i++; или даже *p++ = f( *p ); Ответ: нет, стандарт не предусматривает, какая из частей присваивания вычисляется первой: левая или правая. Поэтому все может работать так, как мы и подразумевали, но может и иначе! Какое i используется в s[i]: 0 или уже 1 (++ уже сделан или нет), то есть int i = 0; s[i] = i++; это s[0] = 0; или же s[1] = 0; ? Какое p будет использовано в левой части *p: уже продвинутое или старое? Еще более эта идея драматизирована в s[i++] = p[i++]; Заметим еще, что в int i=0, j=0; s[i++] = p[j++]; такой проблемы не возникает, поскольку индексы обоих в частях присваивания незави- симы. Зато аналогичная проблема встает в if( a[i++] < b[i] )...; Порядок вычисления операндов не определен, поэтому неясно, что будет сделано прежде: взято значение b[i] или значение a[i++] (тогда будет взято b[i+1] ). Надо писать так, чтобы не полагаться на особенности вашего компилятора: if( a[i] < b[i+1] )...; или *p = *(p+1); i++; ++p; А. Богатырев, 1992-95 - 36 - Си в UNIX Твердо усвойте, что i++ и ++i не только выдают значения i и i+1 соответственно, но и изменяют значение i. Поэтому эти операторы НЕ НАДО использовать там, где по смыслу требуется i+1, а не i=i+1. Так для сравнения соседних элементов массива if( a[i] < a[i+1] ) ... ; /* верно */ if( a[i] < a[++i] ) ... ; /* неверно */ 1.83. Порядок вычисления операндов в бинарных выражениях не определен (что раньше вычисляется - левый операнд или же правый ?). Так пример int f(x,s) int x; char *s; { printf( "%s:%d ", s, x ); return x; } main(){ int x = 1; int y = f(x++, "f1") + f(x+=2, "f2"); printf("%d\n", y); } может печатать либо f1:1 f2:4 5 либо f2:3 f1:3 6 в зависимости от особенностей поведения вашего компилятора (какая из двух f() выпол- нится первой: левая или правая?). Еще пример: int y = 2; int x = ((y = 4) * y ); printf( "%d\n", x ); Может быть напечатано либо 16, либо 8 в зависимости от поведения компилятора, т.е. данный оператор немобилен. Следует написать y = 4; x = y * y; 1.84. Законен ли оператор f(x++, x++); или f(x, x++); Ответ: Нет, порядок вычисления аргументов функций не определен. По той же причине мы не можем писать f( c = getchar(), c ); а должны писать c = getchar(); f(c, c); (если мы именно это имели в виду). Вот еще пример: ... case '+': push(pop()+pop()); break; case '-': push(pop()-pop()); break; ... А. Богатырев, 1992-95 - 37 - Си в UNIX следует заменить на ... case '+': push(pop()+pop()); break; case '-': { int x = pop(); int y = pop(); push(y - x); break; } ... И еще пример: int x = 0; printf( "%d %d\n", x = 2, x ); /* 2 0 либо 2 2 */ Нельзя также struct pnt{ int x; int y; }arr[20]; int i=0; ... scanf( "%d%d", & arr[i].x, & arr[i++].y ); поскольку i++ может сделаться раньше, чем чтение в x. Еще пример: main(){ int i = 3; printf( "%d %d %d\n", i += 7, i++, i++ ); } который показывает, что на IBM PC |- и PDP-11 |= аргументы функций вычисляются справа налево (пример печатает 12 4 3). Впрочем, другие компиляторы могут вычислять их слева направо (как и подсказывает нам здравый смысл). 1.85. Программа печатает либо x=1 либо x=0 в зависимости от КОМПИЛЯТОРА - вычисля- ется ли раньше правая или левая часть оператора вычитания: #include <stdio.h> void main(){ int c = 1; int x = c - c++; printf( "x=%d c=%d\n", x, c ); exit(0); } Что вы имели в виду ? left = c; right = c++; x = left - right; или right = c++; left = c; x = left - right; А если компилятор еще и распараллелит вычисление left и right - то одна программа в разные моменты времени сможет давать разные результаты. ____________________ |- IBM ("Ай-би-эм") - International Buisiness Machines Corporation. Персональные компьютеры IBM PC построены на базе микропроцессоров фирмы Intel. |= PDP-11 - (Programmed Data Processor) - компьютер фирмы DEC (Digital Equipment Corporation), у нас известный как СМ-1420. Эта же фирма выпускает машину VAX. А. Богатырев, 1992-95 - 38 - Си в UNIX Вот еще достойная задачка: x = c-- - --c; /* c-----c */ 1.86. Напишите программу, которая устанавливает в 1 бит 3 и сбрасывает в 0 бит 6. Биты в слове нумеруются с нуля справа налево. Ответ: int x = 0xF0; x |= (1 << 3); x &= ~(1 << 6); В программах часто используют битовые маски как флаги некоторых параметров (признак - есть или нет). Например: #define A 0x08 /* вход свободен */ #define B 0x40 /* выход свободен */ установка флагов : x |= A|B; сброс флагов : x &= ~(A|B); проверка флага A : if( x & A ) ...; проверка, что оба флага есть: if((x & (A|B)) == (A|B))...; проверка, что обоих нет : if((x & (A|B)) == 0 )...; проверка, что есть хоть один: if( x & (A|B))...; проверка, что есть только A : if((x & (A|B)) == A)...; проверка, в каких флагах различаются x и y : diff = x ^ y; 1.87. В программах иногда требуется использовать "множество": каждый допустимый эле- мент множества имеет номер и может либо присутствовать в множестве, либо отсутство- вать. Число вхождений не учитывается. Множества принято моделировать при помощи битовых шкал: #define SET(n,a) (a[(n)/BITS] |= (1L <<((n)%BITS))) #define CLR(n,a) (a[(n)/BITS] &= ~(1L <<((n)%BITS))) #define ISSET(n,a) (a[(n)/BITS] & (1L <<((n)%BITS))) #define BITS 8 /* bits per char (битов в байте) */ /* Перечислимый тип */ enum fruit { APPLE, PEAR, ORANGE=113, GRAPES, RAPE=125, CHERRY}; /* шкала: n из интервала 0..(25*BITS)-1 */ static char fr[25]; main(){ SET(GRAPES, fr); /* добавить в множество */ if(ISSET(GRAPES, fr)) printf("here\n"); CLR(GRAPES, fr); /* удалить из множества */ } 1.88. Напишите программу, распечатывающую все возможные перестановки массива из N элементов. Алгоритм будет рекурсивным