ни есть */ while( next_arrangement (res)) print_arrangement(res, m); clean_iterator(res); 1.89. Напишите макроопределения циклического сдвига переменной типа unsigned int на skew бит влево и вправо (ROL и ROR). Ответ: #define BITS 16 /* пусть целое состоит из 16 бит */ #define ROL(x,skew) x=(x<<(skew))|(x>>(BITS-(skew))) #define ROR(x,skew) x=(x>>(skew))|(x<<(BITS-(skew))) А. Богатырев, 1992-95 - 40 - Си в UNIX Вот как работает ROL(x, 2) при BITS=6 |abcdef| исходно abcdef00 << 2 0000abcdef >> 4 ------ операция | cdefab результат В случае signed int потребуется накладывать маску при сдвиге вправо из-за того, что левые биты при >> не заполняются нулями. Приведем пример для сдвига переменной типа signed char (по умолчанию все char - знаковые) на 1 бит влево: #define CHARBITS 8 #define ROLCHAR1(x) x=(x<<1)|((x>>(CHARBITS-1)) & 01) соответственно для сдвига на 2 бита надо делать & 03 на 3 & 07 на 4 & 017 на skew & ~(~0 << skew) 1.90. Напишите программу, которая инвертирует (т.е. заменяет 1 на 0 и наоборот) N битов, начинающихся с позиции P, оставляя другие биты без изменения. Возможный ответ: unsigned x, mask; mask = ~(~0 << N) << P; x = (x & ~mask) | (~x & mask); /* xnew */ Где маска получается так: ~0 = 11111....11111 ~0 << N = 11111....11000 /* N нулей */ ~(~0 << N) = 00000....00111 /* N единиц */ ~(~0 << N) << P = 0...01110...00 /* N единиц на местах P+N-1..P */ 1.91. Операции умножения * и деления / и % обычно достаточно медленны. В критичных по скорости функциях можно предпринять некоторые ручные оптимизации, связанные с представлением чисел в двоичном коде (хороший компилятор делает это сам!) - пользуясь тем, что операции +, &, >> и << гораздо быстрее. Пусть у нас есть unsigned int x; (для signed операция >> может не заполнять освобождающиеся левые биты нулем!) и 2**n означает 2 в степени n. Тогда: x * (2**n) = x << n x / (2**n) = x >> n x % (2**n) = x - ((x >> n) << n) x % (2**n) = x & (2**n - 1) это 11...111 n двоичных единиц Например: А. Богатырев, 1992-95 - 41 - Си в UNIX x * 8 = x << 3; x / 8 = x >> 3; /* деление нацело */ x % 8 = x & 7; /* остаток от деления */ x * 80 = x*64 + x*16 = (x << 6) + (x << 4); x * 320 = (x * 80) * 4 = (x * 80) << 2 = (x << 8) + (x << 6); x * 21 = (x << 4) + (x << 2) + x; x & 1 = x % 2 = четное(x)? 0:1 = нечетное(x)? 1:0; x & (-2) = x & 0xFFFE = | если x = 2*k то 2*k | если x = 2*k + 1 то 2*k | то есть округляет до четного Или формула для вычисления количества дней в году (високосный/простой): days_in_year = (year % 4 == 0) ? 366 : 365; заменяем на days_in_year = ((year & 0x03) == 0) ? 366 : 365; Вот еще одно полезное равенство: x = x & (a|~a) = (x & a) | (x & ~a) = (x&a) + (x&~a) из чего вытекает, например x - (x % 2**n) = x - (x & (2**n - 1)) = = x & ~(2**n - 1) = (x>>n) << n x - (x%8) = x-(x&7) = x & ~7 Последняя строка может быть использована в функции untab() в главе "Текстовая обра- ботка". 1.92. Обычно мы вычисляем min(a,b) так: #define min(a, b) (((a) < (b)) ? (a) : (b)) или более развернуто if(a < b) min = a; else min = b; Здесь есть операция сравнения и условный переход. Однако, если (a < b) эквивалентно условию (a - b) < 0, то мы можем избежать сравнения. Это предположение верно при (unsigned int)(a - b) <= 0x7fffffff. что, например, верно если a и b - оба неотрицательные числа между 0 и 0x7fffffff. При этих условиях min(a, b) = b + ((a - b) & ((a - b) >> 31)); Как это работает? Рассмотрим два случая: А. Богатырев, 1992-95 - 42 - Си в UNIX Случай 1: a < b Здесь (a - b) < 0, поэтому старший (левый, знаковый) бит разности (a - b) равен 1. Следовательно, (a - b) >> 31 == 0xffffffff, и мы имеем: min(a, b) = b + ((a - b) & ((a - b) >> 31)) = b + ((a - b) & (0xffffffff)) = b + (a - b) = a что корректно. Случай 2: a >= b Здесь (a - b) >= 0, поэтому старший бит разности (a - b) равен 0. Тогда (a - b) >> 31 == 0, и мы имеем: min(a, b) = b + ((a - b) & ((a - b) >> 31)) = b + ((a - b) & (0x00000000)) = b + (0) = b что также корректно. Статья предоставлена by Jeff Bonwick. 1.93. Есть ли быстрый способ определить, является ли X степенью двойки? Да, есть. int X является степенью двойки тогда и только тогда, когда (X & (X - 1)) == 0 (в частности 2 здесь окажется степенью двойки). Как это работает? Пусть X != 0. Если X - целое, то его двоичное представление таково: X = bbbbbbbbbb10000... где 'bbb' представляет некие биты, '1' - младший бит, и все остальные биты правее - нули. Поэтому: X = bbbbbbbbbb10000... X - 1 = bbbbbbbbbb01111... ------------------------------------ X & (X - 1) = bbbbbbbbbb00000... Другими словами, X & (X-1) имеет эффект обнуления последнего единичного бита. Если X - степень двойки, то он содержит в двоичном представлении ровно ОДИН такой бит, поэ- тому его гашение обращает результат в ноль. Если X - не степень двойки, то в слове есть хотя бы ДВА единичных бита, поэтому X & (X-1) должно содержать хотя бы один из оставшихся единичных битов - то есть не равняться нулю. Следствием этого служит программа, вычисляющая число единичных битов в слове X: int popc; for (popc = 0; X != 0; X &= X - 1) popc++; При этом потребуется не 32 итерации (число бит в int), а ровно столько, сколько еди- ничных битов есть в X. Статья предоставлена by Jeff Bonwick. А. Богатырев, 1992-95 - 43 - Си в UNIX 1.94. Функция для поиска номера позиции старшего единичного бита в слове. Использу- ется бинарный поиск: позиция находится максимум за 5 итераций (двоичный логарифм 32х), вместо 32 при линейном поиске. int highbit (unsigned int x) { int i; int h = 0; for (i = 16; i >= 1; i >>= 1) { if (x >> i) { h += i; x >>= i; } } return (h); } Статья предоставлена by Jeff Bonwick. 1.95. Напишите функцию, округляющую свой аргумент вниз до степени двойки. #include <stdio.h> #define INT short #define INFINITY (-999) /* Функция, выдающая число, являющееся округлением вниз * до степени двойки. * Например: * 0000100010111000110 * заменяется на * 0000100000000000000 * то есть остается только старший бит. * В параметр power2 возвращается номер бита, * то есть показатель степени двойки. Если число == 0, * то эта степень равна минус бесконечности. */ А. Богатырев, 1992-95 - 44 - Си в UNIX unsigned INT round2(unsigned INT x, int *power2){ /* unsigned - чтобы число рассматривалось как * битовая шкала, а сдвиг >> заполнял левые биты * нулем, а не расширял вправо знаковый бит. * Идея функции: сдвигать число >> пока не получится 1 * (можно было бы выбрать 0). * Затем сдвинуть << на столько же разрядов, при этом все правые * разряды заполнятся нулем, что и требовалось. */ int n = 0; if(x == 0){ *power2 = -INFINITY; return 0; } if(x == 1){ *power2 = 0; return 1; } while(x != 1){ x >>= 1; n++; if(x == 0 || x == (unsigned INT)(-1)){ printf("Вижу %x: похоже, что >> расширяет знаковый бит.\n" "Зациклились!!!\n", x); return (-1); } } x <<= n; *power2 = n; return x; } int counter[ sizeof(unsigned INT) * 8]; int main(void){ unsigned INT i; int n2; for(i=0; ; i++){ round2(i, &n2); if(n2 == -INFINITY) continue; counter[n2]++; /* Нельзя писать for(i=0; i < (unsigned INT)(-1); i++) * потому что такой цикл бесконечен! */ if(i == (unsigned INT) (-1)) break; } for(i=0; i < sizeof counter/sizeof counter[0]; i++) printf("counter[%u]=%d\n", i, counter[i]); return 0; } 1.96. Если некоторая вычислительная функция будет вызываться много раз, не следует пренебрегать возможностью построить таблицу решений, где значение вычисляется один раз для каждого входного значения, зато потом берется непосредственно из таблицы и не вычисляется вообще. Пример: подсчет числа единичных бит в байте. Напоминаю: байт состоит из 8 бит. А. Богатырев, 1992-95 - 45 - Си в UNIX #include <stdio.h> int nbits_table[256]; int countBits(unsigned char c){ int nbits = 0; int bit; for(bit = 0; bit < 8; bit++){ if(c & (1 << bit)) nbits++; } return nbits; } void generateTable(){ int c; for(c=0; c < 256; c++){ nbits_table[ (unsigned char) c ] = countBits(c); /* printf("%u=%d\n", c, nbits_table[ c & 0377 ]); */ } } int main(void){ int c; unsigned long bits = 0L; unsigned long bytes = 0L; generateTable(); while((c = getchar()) != EOF){ bytes++; bits += nbits_table[ (unsigned char) c ]; } printf("%lu байт\n", bytes); printf("%lu единичных бит\n", bits); printf("%lu нулевых бит\n", bytes*8 - bits); return 0; } 1.97. Напишите макрос swap(x, y), обменивающий значениями два своих аргумента типа int. #define swap(x,y) {int tmp=(x);(x)=(y);(y)=tmp;} ... swap(A, B); ... Как можно обойтись без временной переменной? Ввиду некоторой курьезности последнего способа, приводим ответ: int x, y; /* A B */ x = x ^ y; /* A^B B */ y = x ^ y; /* A^B A */ x = x ^ y; /* B A */ Здесь используется тот факт, что A^A дает 0. 1.98. Напишите функцию swap(x, y) при помощи указателей. Заметьте, что в отличие от макроса ее придется вызывать как А. Богатырев, 1992-95 - 46 - Си в UNIX ... swap(&A, &B); ... Почему? 1.99. Пример объясняет разницу между формальным и фактическим параметром. Термин "формальный" означает, что имя параметра можно произвольно заменить другим (во всем теле функции), т.е. само имя не существенно. Так f(x,y) { return(x + y); } и f(муж,жена) { return(муж + жена); } воплощают одну и ту же функцию. "Фактический" - означает значение, даваемое пара- метру в момент вызова функции: f(xyz, 43+1); В Си это означает, что формальным параметрам (в качестве локальных переменных) прис- ваиваются начальные значения, равные значениям фактических параметров: x = xyz; y = 43 + 1; /*в теле ф-ции их можно менять*/ При выходе из функции формальные параметры (и локальные переменные) разопределяются (и даже уничтожаются, см. следующий параграф). Имена формальных параметров могут "перекрывать" (делать невидимыми, override) одноименные глобальные переменные на время выполнения данной функции. Что печатает программа? char str[] = "строка1"; char lin[] = "строка2"; f(str) char str[]; /* формальный параметр. */ { printf( "%s %s\n", str, str ); } main(){ char *s = lin; /* фактический параметр: */ f(str); /* массив str */ f(lin); /* массив lin */ f(s); /* переменная s */ f("строка3"); /* константа */ f(s+2); /* значение выражения */ } Обратите внимание, что параметр str из f(str) и массив str[] - это две совершенно РАЗНЫЕ вещи, хотя и называющиеся одинаково. Переименуйте аргумент функции f и пере- пишите ее в виде f(ss) char ss[]; /* формальный параметр. */ { printf( "%s %s\n", ss, str ); } Что печатается теперь? Составьте аналогичный пример с целыми числами. 1.100. Поговорим более подробно про область видимости имен. int x = 12; f(x){ int y = x*x; if(x) f(x - 1); } main(){ int x=173, z=21; f(2); } Локальные переменные и аргументы функции отводятся в стеке при вызове функции и А. Богатырев, 1992-95 - 47 - Си в UNIX уничтожаются при выходе из нее: -+ +- вершина стека |локал y=0 | |аргумент x=0 | f(0) |---------------|--------- "кадр" |локал y=1 | frame |аргумент x=1 | f(1) |---------------|--------- |локал y=4 | |аргумент x=2 | f(2) |---------------|--------- |локал z=21 | auto: |локал x=173 | main() ================================== дно стека static: глобал x=12 ================================== Автоматические локальные переменные и аргументы функции видимы только в том вызове функции, в котором они отведены; но не видимы ни в вызывающих, ни в вызываемых функ- циях (т.е. видимость их ограничена рамками своего "кадра" стека). Статические гло- бальные переменные видимы в любом кадре, если только они не "перекрыты" (заслонены) одноименной локальной переменной (или формалом) в данном кадре. Что напечатает программа? Постарайтесь ответить на этот вопрос не выполняя программу на машине! x1 x2 x3 x4 x5 int x = 12; /* x1 */ | . . . . f(){ |___ . . . int x = 8; /* x2, перекрытие */ : | . . . printf( "f: x=%d\n", x ); /* x2 */ : | . . . x++; /* x2 */ : | . . . } :--+ . . . g(x){ /* x3 */ :______ . . printf( "g: x=%d\n", x ); /* x3 */ : | . . x++; /* x3 */ : | . . } :-----+ . . h(){ :_________ . int x = 4; /* x4 */ : | . g(x); /* x4 */ : |___ { int x = 55; } /* x5 */ : : | printf( "h: x=%d\n", x ); /* x4 */ : |--+ } :--------+ main(){ | f(); h(); | printf( "main: x=%d\n", x ); /* x1 */ | } ---- Ответ: f: x=8 g: x=4 h: x=4 main: x=12 Обратите внимание на функцию g. Аргументы функции служат копиями фактических пара- метров (т.е. являются локальными переменными функции, проинициализированными значени- ями фактических параметров), поэтому их изменение не приводит к изменению фактичес- кого параметра. Чтобы изменять фактический параметр, надо передавать его адрес! А. Богатырев, 1992-95 - 48 - Си в UNIX 1.101. Поясним последнюю фразу. (Внимание! Возможно, что данный пункт вам следует читать ПОСЛЕ главы про указатели). Пусть мы хотим написать функцию, которая обмени- вает свои аргументы x и y так, чтобы выполнялось x < y. В качестве значения функция будет выдавать (x+y)/2. Если мы напишем так: int msort(x, y) int x, y; { int tmp; if(x > y){ tmp=x; x=y; y=tmp; } return (x+y)/2; } int x=20, y=8; main(){ msort(x,y); printf("%d %d\n", x, y); /* 20 8 */ } то мы не достигнем желаемого эффекта. Здесь переставляются x и y, которые являются локальными переменными, т.е. копиями фактических параметров. Поэтому вне функции эта перестановка никак не проявляется! Чтобы мы могли изменить аргументы, копироваться в локальные переменные должны не сами значения аргументов, а их адреса: int msort(xptr, yptr) int *xptr, *yptr; { int tmp; if(*xptr > *yptr){tmp= *xptr;*xptr= *yptr;*yptr=tmp;} return (*xptr + *yptr)/2; } int x=20, y=8, z; main(){ z = msort(&x,&y); printf("%d %d %d\n", x, y, z); /* 8 20 14 */ } Обратите внимание, что теперь мы передаем в функцию не значения x и y, а их адреса &x и &y. Именно поэтому (чтобы x смог измениться) стандартная функция scanf() требует указания адресов: int x; scanf("%d", &x); /* но не scanf("%d", x); */ Заметим, что адрес от арифметического выражения или от константы (а не от переменной) вычислить нельзя, поэтому законны: int xx=12, *xxptr = &xx, a[2] = { 13, 17 }; int *fy(){ return &y; } msort(&x, &a[0]); msort(a+1, xxptr); msort(fy(), xxptr); но незаконны msort(&(x+1), &y); и msort(&x, &17); Заметим еще, что при работе с адресами мы можем направить указатель в неверное место и получить непредсказуемые результаты: msort(&xx - 20, a+40); (указатели указывают неизвестно на что). Резюме: если аргумент служит только для передачи значения В функцию - его не надо (хотя и можно) делать указателем на переменную, содержащую требуемое значение (если только это уже не указатель). Если же аргумент служит для передачи значения ИЗ функции - он должен быть указателем на переменную возвращаемого типа (лучше А. Богатырев, 1992-95 - 49 - Си в UNIX возвращать значение как значение функции - return-ом, но иногда надо возвращать нес- колько значений - и этого главного "окошка" не хватает). Контрольный вопрос: что печатает фрагмент? int a=2, b=13, c; int f(x, y, z) int x, *y, z; { *y += x; x *= *y; z--; return (x + z - a); } main(){ c=f(a, &b, a+4); printf("%d %d %d\n",a,b,c); } (Ответ: 2 15 33) 1.102. Формальные аргументы функции - это такие же локальные переменные. Параметры как бы описаны в самом внешнем блоке функции: char *func1(char *s){ int s; /* ошибка: повторное определение имени s */ ... } int func2(int x, int y){ int z; ... } соответствует int func2(){ int x = безымянный_аргумент_1_со_стека; int y = безымянный_аргумент_2_со_стека; int z; ... } Мораль такова: формальные аргументы можно смело изменять и использовать как локальные переменные. 1.103. Все параметры функции можно разбить на 3 класса: - in - входные; - out - выходные, служащие для возврата значения из функции; либо для изменения данных, находящихся по этому адресу; - in/out - для передачи значения в функцию и из функции. Два последних типа параметров должны быть указателями. Иногда (особенно в прототипах и в документации) бывает полезно указывать класс параметра в виде комментария: int f( /*IN*/ int x, /*OUT*/ int *yp, /*INOUT*/ int *zp){ *yp = ++x + ++(*zp); return (*zp *= x) - 1; } int x=2, y=3, z=4, res; main(){ res = f(x, &y, &z); printf("res=%d x=%d y=%d z=%d\n",res,x,y,z); /* 14 2 8 15 */ } Это полезно потому, что иногда трудно понять - зачем параметр описан как указатель. То ли по нему выдается из функции информация, то ли это просто указатель на данные (массив), передаваемые в функцию. В первом случае указуемые данные будут изменены, а во втором - нет. В первом случае указатель должен указывать на зарезервированную нами А. Богатырев, 1992-95 - 50 - Си в UNIX область памяти, в которой будет размещен результат. Пример на эту тему есть в главе "Текстовая обработка" (функция bi_conv). 1.104. Известен такой стиль оформления аргументов функции: void func( int arg1 , char *arg2 /* argument 2 */ , char *arg3[] , time_t time_stamp ){ ... } Суть его в том, что запятые пишутся в столбик и в одну линию с ( и ) скобками для аргументов. При таком стиле легче добавлять и удалять аргументы, чем при версии с запятой в конце. Этот же стиль применим, например, к перечислимым типам: enum { red , green , blue }; Напишите программу, форматирующую заголовки функций таким образом. 1.105. В чем ошибка? char *val(int x){ char str[20]; sprintf(str, "%d", x); return str; } void main(){ int x = 5; char *s = val(x); printf("The values:\n"); printf("%d %s\n", x, s); } Ответ: val возвращает указатель на автоматическую переменную. При выходе из функции val() ее локальные переменные (в частности str[]) в стеке уничтожаются - указатель s теперь указывает на испорченные данные! Возможным решением проблемы является превра- щение str[] в статическую переменную (хранимую не в стеке): static char str[20]; Однако такой способ не позволит писать конструкции вида printf("%s %s\n", val(1), val(2)); так как под оба вызова val() используется один и тот же буфер str[] и будет печа- таться "1 1" либо "2 2", но не "1 2". Более правильным будет задание буфера для результата val() как аргумента: char *val(int x, char str[]){ sprintf(str, "%d", x); return str; } void main(){ int x=5, y=7; char s1[20], s2[20]; printf("%s %s\n", val(x, s1), val(y, s2)); } А. Богатырев, 1992-95 - 51 - Си в UNIX 1.106. Каковы ошибки (не синтаксические) в программе|-? main() { double y; int x = 12; y = sin (x); printf ("%s\n", y); } Ответ: - стандартная библиотечная функция sin() возвращает значение типа double, но мы нигде не информируем об этом компилятор. Поэтому он считает по умолчанию, что эта функция возвращает значение типа int и делает в присваивании y=sin(x) приве- дение типа int к типу левого операнда, т.е. к double. В результате возвращаемое значение (а оно на самом деле - double) интерпретируется неверно (как int), под- вергается приведению типа (которое портит его), и результат получается совер- шенно не таким, как надо. Подобная же ошибка возникает при использовании функ- ций, возвращающих указатель, например, функций malloc() и itoa(). Поэтому если мы пользуемся библиотечной функцией, возвращающей не int, мы должны предвари- тельно (до первого использования) описать ее, например|=: extern double sin(); extern long atol(); extern char *malloc(), *itoa(); Это же относится и к нашим собственным функциям, которые мы используем прежде, чем определяем (поскольку из заголовка функции компилятор обнаружит, что она выдает не целое значение, уже после того, как странслирует обращение к ней): /*extern*/ char *f(); main(){ char *s; s = f(1); puts(s); } char *f(n){ return "knights" + n; } Функции, возвращающие целое, описывать не требуется. Описания для некоторых стандартных функций уже помещены в системные include-файлы. Например, описания для математических функций (sin, cos, fabs, ...) содержатся в файле /usr/include/math.h. Поэтому мы могли бы написать перед main #include <math.h> вместо extern double sin(), cos(), fabs(); - библиотечная функция sin() требует аргумента типа double, мы же передаем ей аргумент типа int (который короче типа double и имеет иное внутреннее представ- ление). Он будет неправильно проинтерпретирован функцией, т.е. мы вычислим синус отнюдь НЕ числа 12. Следует писать: y = sin( (double) x ); и sin(12.0); вместо sin(12); ____________________ |- Для трансляции программы, использующей стандартные математические функции sin, cos, exp, log, sqrt, и.т.п. следует задавать ключ компилятора -lm cc file.c -o file -lm |= Слово extern ("внешняя") не является обязательным, но является признаком хоро- шего тона - вы сообщаете программисту, читающему эту программу, что данная функция реализована в другом файле, либо вообще является стандартной и берется из библиотеки. А. Богатырев, 1992-95 - 52 - Си в UNIX - в printf мы печатаем значение типа double по неправильному формату: следует использовать формат %g или %f (а для ввода при помощи scanf() - %lf). Очень частой ошибкой является печать значений типа long по формату %d вместо %ld . Первых двух проблем в современном Си удается избежать благодаря заданию прототипов функций (о них подробно рассказано ниже, в конце главы "Текстовая обработка"). Нап- ример, sin имеет прототип double sin(double x); Третяя проблема (ошибка в формате) не может быть локализована средствами Си и имеет более-менее приемлемое решение лишь в языке C++ (streams). 1.107. Найдите ошибку: int sum(x,y,z){ return(x+y+z); } main(){ int s = sum(12,15); printf("%d\n", s); } Заметим, что если бы для функции sum() был задан прототип, то компилятор поймал бы эту нашу оплошность! Заметьте, что сейчас значение z в sum() непредсказуемо. Если бы мы вызывали s = sum(12,15,17,24); то лишние аргументы были бы просто проигнорированы (но и тут может быть сюрприз - аргументы могли бы игнорироваться с ЛЕВОГО конца списка!). А вот пример опасной ошибки, которая не ловится даже прототипами: int x; scanf("%d%d", &x ); Второе число по формату %d будет считано неизвестно по какому адресу и разрушит память программы. Ни один компилятор не проверяет соответствие числа %-ов в строке формата числу аргументов scanf и printf. 1.108. Что здесь означают внутренние (,,) в вызове функции f() ? f(x, y, z){ printf("%d %d %d\n", x, y, z); } main(){ int t; f(1, (2, 3, 4), 5); f(1, (t=3,t+1), 5); } Ответ: (2,3,4) - это оператор "запятая", выдающий значение последнего выражения из списка перечисленных через запятую выражений. Здесь будет напечатано 1 4 5. Кажущаяся двойственность возникает из-за того, что аргументы функции тоже перечисляются через запятую, но это совсем другая синтаксическая конструкция. Вот еще пример: int y = 2, x; x = (y+4, y, y*2); printf("%d\n", x); /* 4 */ x = y+4, y, y*2 ; printf("%d\n", x); /* 6 */ x = (x=y+4, ++y, x*y); printf("%d\n", x); /* 18 */ Сначала обратим внимание на первую строку. Это - объявление переменных x и y (причем y - с инициализацией), поэтому запятая здесь - не ОПЕРАТОР, а просто разделитель объявляемых переменных! Далее следуют три строки выполняемых операторов. В первом случае выполнилось x=y*2; во втором x=y+4 (т.к. приоритет у присваивания выше, чем у А. Богатырев, 1992-95 - 53 - Си в UNIX запятой). Обратите внимание, что выражение без присваивания (которое может вообще не иметь эффекта или иметь только побочный эффект) вполне законно: x+y; или z++; или x == y+1; или x; В частности, все вызовы функций-процедур именно таковы (это выражения без оператора присваивания, имеющие побочный эффект): f(12,x); putchar('Ы'); в отличие, скажем, от x=cos(0.5)/3.0; или c=getchar(); Оператор "запятая" разделяет выражения, а не просто операторы, поэтому если хоть один из перечисленных операторов не выдает значения, то это является ошибкой: main(){ int i, x = 0; for(i=1; i < 4; i++) x++, if(x > 2) x = 2; /* используй { ; } */ } оператор if не выдает значения. Также логически ошибочно использование функции типа void (не возвращающей значения): void f(){} ... for(i=1; i < 4; i++) x++, f(); хотя компилятор может допустить такое использование. Вот еще один пример того, как можно переписать один и тот же фрагмент, применяя разные синтаксические конструкции: if( условие ) { x = 0; y = 0; } if( условие ) x = 0, y = 0; if( условие ) x = y = 0; 1.109. Найдите опечатку: switch(c){ case 1: x++; break; case 2: y++; break; defalt: z++; break; } Если c=3, то z++ не происходит. Почему? (Потому, что defalt: - это метка, а не клю- чевое слово default). 1.110. Почему программа зацикливается и печатает совсем не то, что нажато на клавиа- туре, а только 0 и 1? while ( c = getchar() != 'e') printf("%d %c\n, c, c); Ответ: данный фрагмент должен был выглядеть так: while ((c = getchar()) != 'e') printf("%d %c\n, c, c); А. Богатырев, 1992-95 - 54 - Си в UNIX Сравнение в Си имеет высший приоритет, нежели присваивание! Мораль: надо быть внима- тельнее к приоритетам операций. Еще один пример на похожую тему: вместо if( x & 01 == 0 ) ... if( c&0377 > 0300)...; надо: if( (x & 01) == 0 ) ... if((c&0377) > 0300)...; И еще пример с аналогичной ошибкой: FILE *fp; if( fp = fopen( "файл", "w" ) == NULL ){ fprintf( stderr, "не могу писать в файл\n"); exit(1); } fprintf(fp,"Good bye, %s world\n","cruel"); fclose(fp); В этом примере файл открывается, но fp равно 0 (логическое значение!) и функция fprintf() не срабатывает (программа падает по защите памяти|-). Исправьте аналогичную ошибку (на приоритет операций) в следующей функции: /* копирование строки from в to */ char *strcpy( to, from ) register char *from, *to; { char *p = to; while( *to++ = *from++ != '\0' ); return p; } 1.111. Сравнения с нулем (0, NULL, '\0') в Си принято опускать (хотя это не всегда способствует ясности). if( i == 0 ) ...; --> if( !i ) ... ; if( i != 0 ) ...; --> if( i ) ... ; например, вместо char s[20], *p ; for(p=s; *p != '\0'; p++ ) ... ; будет for(p=s; *p; p++ ) ... ; и вместо char s[81], *gets(); while( gets(s) != NULL ) ... ; будет while( gets(s)) ... ; Перепишите strcpy в этом более лаконичном стиле. ____________________ |- "Падать" - программистский жаргон. Означает "аварийно завершаться". "Защита па- мяти" - обращение по некорректному адресу. В UNIX такая ошибка ловится аппаратно, и программа будет убита одним из сигналов: SIGBUS, SIGSEGV, SIGILL. Система сообщит нечто вроде "ошибка шины". Знайте, что это не ошибка аппаратуры и не сбой, а ВАША ошибка! А. Богатырев, 1992-95 - 55 - Си в UNIX 1.112. Истинно ли выражение if( 2 < 5 < 4 ) Ответ: да! Дело в том, что Си не имеет логического типа, а вместо "истина" и "ложь" использует целые значения "не 0" и "0" (логические операции выдают 1 и 0). Данное выражение в условии if эквивалентно следующему: ((2 < 5) < 4) Значением (2 < 5) будет 1. Значением (1 < 4) будет тоже 1 (истина). Таким образом мы получаем совсем не то, что ожидалось. Поэтому вместо if( a < x < b ) надо писать if( a < x && x < b ) 1.113. Данная программа должна печатать коды вводимых символов. Найдите опечатку; почему цикл сразу завершается? int c; for(;;) { printf("Введите очередной символ:"); c = getchar(); if(c = 'e') { printf("нажато e, конец\n"); break; } printf( "Код %03o\n", c & 0377 ); } Ответ: в if имеется опечатка: использовано `=' вместо `=='. Присваивание в Си (а также операции +=, -=, *=, и.т.п.) выдает новое значение левой части, поэтому синтаксической ошибки здесь нет! Написанный оператор равносилен c = 'e'; if( c ) ... ; и, поскольку 'e'!= 0, то условие оказывается истинным! Это еще и следствие того, что в Си нет специального логического типа (истина/ложь). Будьте внимательны: компилятор не считает ошибкой использование оператора = вместо == внутри условий if и условий циклов (хотя некоторые компиляторы выдают предупреждение). Еще аналогичная ошибка: for( i=0; !(i = 15) ; i++ ) ... ; (цикл не выполняется); или static char s[20] = " abc"; int i=0; while(s[i] = ' ') i++; printf("%s\n", &s[i]); /* должно напечататься abc */ (строка заполняется пробелами и цикл не кончается). То, что оператор присваивания имеет значение, весьма удобно: int x, y, z; это на самом деле x = y = z = 1; x = (y = (z = 1)); А. Богатырев, 1992-95 - 56 - Си в UNIX или|- y=f( x += 2 ); // вместо x+=2; y=f(x); if((y /= 2) > 0)...; // вместо y/=2; if(y>0)...; Вот пример упрощенной игры в "очко" (упрощенной - т.к. не учитывается ограниченность числа карт каждого типа в колоде (по 4 штуки)): #include <stdio.h> main(){ int sum = 0, card; char answer[36]; srand( getpid()); /* рандомизация */ do{ printf( "У вас %d очков. Еще? ", sum); if( *gets(answer) == 'n' ) break; /* иначе маловато будет */ printf( " %d очков\n", card = 6 + rand() % (11 - 6 + 1)); } while((sum += card) < 21); /* SIC ! */ printf ( sum == 21 ? "очко\n" : sum > 21 ? "перебор\n": "%d очков\n", sum); } Вот еще пример, использующийся для подсчета правильного размера таблицы. Обратите внимание, что присваивания используются в сравнениях, в аргументах вызова функции (printf), т.е. везде, где допустимо выражение: #include <stdio.h> int width = 20; /* начальное значение ширины поля */ int len; char str[512]; main(){ while(gets(str)){ if((len = strlen(str)) > width){ fprintf(stderr,"width увеличить до %d\n", width=len); } printf("|%*.*s|\n", -width, width, str); } } Вызывай эту программу как a.out < входнойФайл > /dev/null 1.114. Почему программа "зависает" (на самом деле - зацикливается) ? int x = 0; while( x < 100 ); printf( "%d\n", x++ ); printf( "ВСЕ\n" ); Указание: где кончается цикл while? Мораль: не надо ставить ; где попало. Еще мораль: даже отступы в оформлении программы не являются гарантией отсутствия ошибок в группировке операторов. 1.115. Вообще, приоритеты операций в Си часто не соответствуют ожиданиям нашего здравого смысла. Например, значением выражения: x = 1 << 2 + 1 ; ____________________ |- Конструкция //текст, которая будет изредка попадаться в дальнейшем - это коммен- тарий в стиле языка C++. Такой комментарий простирается от символа // до конца строки. А. Богатырев, 1992-95 - 57 - Си в UNIX будет 8, а не 5, поскольку сложение выполнится первым. Мораль: в затруднительных и неочевидных случаях лучше явно указывать приоритеты при помощи круглых скобок: x = (1 << 2) + 1 ; Еще пример: увеличивать x на 40, если установлен флаг, иначе на 1: int bigFlag = 1, x = 2; x = x + bigFlag ? 40 : 1; printf( "%d\n", x ); ответом будет 40, а не 42, поскольку это x = (x + bigFlag) ? 40 : 1; а не x = x + (bigFlag ? 40 : 1); которое мы имели в виду. Поэтому вокруг условного выражения ?: обычно пишут круглые скобки. Заметим, что () указывают только приоритет, но не порядок вычислений. Так, ком- пилятор имеет полное право вычислить long a = 50, x; int b = 4; x = (a * 100) / b; /* деление целочисленное с остатком ! */ и как x = (a * 100)/b = 5000/4 = 1250 и как x = (a/b) * 100 = 12*100 = 1200 невзирая на наши скобки, поскольку и * и / имеют одинаковый приоритет (хотя это "право" еще не означает, что он обязательно так поступит). Такие операторы прихо- дится разбивать на два, т.е. вводить промежуточную переменную: { long a100 = a * 100; x = a100 / b; } 1.116. Составьте программу вычисления тригонометрической функции. Название функции и значение аргумента передаются в качестве параметров функции main (см. про argv и argc в главе "Взаимодействие с UNIX"): $ a.out sin 0.5 sin(0.5)=0.479426 (здесь и далее значок $ обозначает приглашение, выданное интерпретатором команд). Для преобразования строки в значение типа double воспользуйтесь стандартной функцией atof(). char *str1, *str2, *str3; ... extern double at