Ум подобен желудку.
Важно не то, сколько ты в него вложишь,
а то, сколько он сможет переварить.
В этой книге вы найдете ряд задач, примеров, алгоритмов, советов и стилистичес-
ких замечаний по использованию языка программирования "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.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
________________________________________________________________________________
doBODYwhile(CONDITION)
________________________________________________________________________________
cont:
repeat:
BODY;
if(CONDITION) goto repeat;
out: ;
В операторах цикла внутри тела цикла BODY могут присутствовать операторы break и
continue; которые означают на наших схемах следующее:
#define break gotoout
#define continue gotocont1.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 - Си в UNIX1.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 (до десяти раз), программа будет
работать дольше. В повседневных задачах вам вряд ли понадобится иметь счетчик,
отличный от longcnt; (печатать его надо по формату "%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 - Си в UNIX1.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 - Си в UNIX1.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 - Си в UNIX1.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 ) returnx*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
%
Возможно также использование кодов букв:
case0312:
но это гораздо менее наглядно. Подавление знакового бита необходимо также и в опера-
торах 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. Подумайте, почему для
unsigneda, 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 EquipmentCorporation), у нас известный как СМ-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
элементов. Алгоритм будет рекурсивным