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 atof(); double x = atof(str1); extern long atol(); long y = atol(str2); extern int atoi(); int i = atoi(str3); либо sscanf(str1, "%f", &x); sscanf(str2, "%ld", &y); sscanf(str3,"%d", &i); К слову заметим, что обратное преобразование - числа в текст - удобнее всего делается при помощи функции sprintf(), которая аналогична printf(), но сформированная ею строка-сообщение не выдается на экран, а заносится в массив: А. Богатырев, 1992-95 - 58 - Си в UNIX char represent[ 40 ]; int i = ... ; sprintf( represent, "%d", i ); 1.117. Составьте программу вычисления полинома n-ой степени: n n-1 Y = A * X + A * X + ... + A0 n n-1 схема (Горнера): Y = A0 + X * ( A1 + X * ( A2 + ... + X * An )))...) Оформите алгоритм как функцию с переменным числом параметров: poly( x, n, an, an-1, ... a0 ); О том, как это сделать - читайте раздел руководства по UNIX man varargs. Ответ: #include <varargs.h> double poly(x, n, va_alist) double x; int n; va_dcl { va_list args; double sum = 0.0; va_start(args); /* инициализировать список арг-тов */ while( n-- >= 0 ){ sum *= x; sum += va_arg(args, double); /* извлечь след. аргумент типа double */ } va_end(args); /* уничтожить список аргументов */ return sum; } main(){ /* y = 12*x*x + 3*x + 7 */ printf( "%g\n", poly(2.0, 2, 12.0, 3.0, 7.0)); } Прототип этой функции: double poly(double x, int n, ... ); В этом примере использованы макросы va_нечто. Часть аргументов, которая является списком переменной длины, обозначается в списке параметров как va_alist, при этом она объявляется как va_dcl в списке типов параметров. Заметьте, что точка-с-запятой после va_dcl не нужна! Описание va_list args; объявляет специальную "связную" переменную; смысл ее машинно зависим. va_start(args) инициализирует эту переменную списком фак- тических аргументов, соответствующих va_alist-у. va_end(args) деинициализирует эту переменную (это надо делать обязательно, поскольку инициализация могла быть связана с конструированием списка аргументов при помощи выделения динамической памяти; теперь мы должны уничтожить этот список и освободить память). Очередной аргумент типа TYPE извлекается из списка при помощи TYPE x = va_arg(args, TYPE); Список аргументов просматривается слева направо в одном направлении, возврат к А. Богатырев, 1992-95 - 59 - Си в UNIX предыдущему аргументу невозможен. Нельзя указывать в качестве типов char, short, float: char ch = va_arg(args, char); поскольку в языке Си аргументы функции таких типов автоматически расширяются в int, int, double соответственно. Корректно будет так: int ch = va_arg(args, int); 1.118. Еще об одной ловушке в языке Си на PDP-11 (и в компиляторах бывают ошибки!): unsigned x = 2; printf( "%ld %ld", - (long) x, (long) -x ); Этот фрагмент напечатает числа -2 и 65534. Во втором случае при приведении к типу long был расширен знаковый бит. Встроенная операция sizeof выдает значение типа unsigned. Подумайте, каков будет эффект в следующем фрагменте программы? static struct point{ int x, y ;} p = { 33, 13 }; FILE *fp = fopen( "00", "w" ); /* вперед на длину одной структуры */ fseek( fp, (long) sizeof( struct point ), 0 ); /* назад на длину одной структуры */ /*!*/ fseek( fp, (long) -sizeof( struct point ), 1 ); /* записываем в начало файла одну структуру */ fwrite( &p, sizeof p, 1, fp ); /* закрываем файл */ fclose( fp ); Где должен находиться минус во втором вызове fseek для получения ожидаемого резуль- тата? (Данный пример может вести себя по-разному на разных машинах, вопросы касаются PDP-11). 1.119. Обратимся к указателям на функции: void g(x){ printf("%d: here\n", x); } main(){ void (*f)() = g; /* Указатель смотрит на функцию g() */ (*f)(1); /* Старая форма вызова функции по указателю */  f (2); /* Новая форма вызова */ /* В обоих случаях вызывается g(x); */ } Что печатает программа? typedef void (*(*FUN))(); /* Попытка изобразить рекурсивный тип typedef FUN (*FUN)(); */ FUN g(FUN f){ return f; } void main(){ FUN y = g(g(g(g(g)))); if(y == g) printf("OK\n"); А. Богатырев, 1992-95 - 60 - Си в UNIX } Что печатает программа? char *f(){ return "Hello, user!"; } g(func) char * (*func)(); { puts((*func)()); } main(){ g(f); } Почему было бы неверно написать main(){ g(f()); } Еще аналогичная ошибка (посмотрите про функцию signal в главе "Взаимодействие с UNIX"): #include <signal.h> f(){ printf( "Good bye.\n" ); exit(0); } main(){ signal ( SIGINT, f() ); ... } Запомните, что f() - это ЗНАЧЕНИЕ функции f (т.е. она вызывается и нечто возвращает return-ом; это-то значение мы и используем), а f - это АДРЕС функции f (раньше это так и писалось &f), то есть метка начала ее машинных кодов ("точка входа"). 1.120. Что напечатает программа? (Пример посвящен указателям на функции и массивам функций): int f(n){ return n*2; } int g(n){ return n+4; } int h(n){ return n-1; } int (*arr[3])() = { f, g, h }; main(){ int i; for(i=0; i < 3; i++ ) printf( "%d\n", (*arr[i])(i+7) ); } 1.121. Что напечатает программа? extern double sin(), cos(); main(){ double x; /* cc -lm */ for(x=0.0; x < 1.0; x += 0.2) printf("%6.4g %6.4g %6.4g\n", (x > 0.5 ? sin : cos)(x), sin(x), cos(x)); } то же в варианте А. Богатырев, 1992-95 - 61 - Си в UNIX extern double sin(), cos(); main(){ double x; double (*f)(); for(x=0.0; x < 1.0; x += 0.2){ f = (x > 0.5 ? sin : cos); printf("%g\n", (*f)(x)); } } 1.122. Рассмотрите четыре реализации функции факториал: n! = 1 * 2 * ... * n или n! = n * (n-1)! где 0! = 1 Все они иллюстрируют определенные подходы в программировании: /* ЦИКЛ (ИТЕРАЦИЯ) */ int factorial1(n){ int res = 1; while(n > 0){ res *= n--; } return res; } /* ПРОСТАЯ РЕКУРСИЯ */ int factorial2(n){ return (n==0 ? 1 : n * factorial2(n-1)); } /* Рекурсия, в которой функция вызывается рекурсивно * единственный раз - в операторе return, называется * "хвостовой рекурсией" (tail recursion) и * легко преобразуется в цикл */ /* АВТОАППЛИКАЦИЯ */ int fi(f, n) int (*f)(), n; { if(n == 0) return 1; else return n * (*f)(f, n-1); } int factorial3(n){ return fi(fi, n); } /* РЕКУРСИЯ С НЕЛОКАЛЬНЫМ ПЕРЕХОДОМ */ #include <setjmp.h> jmp_buf checkpoint; void fact(n, res) register int n, res; { if(n) fact(n - 1, res * n); else longjmp(checkpoint, res+1); } int factorial4(n){ int res; if(res = setjmp(checkpoint)) return (res - 1); else fact(n, 1); } 1.123. Напишите функцию, печатающую целое число в системе счисления с основанием base. Ответ: А. Богатырев, 1992-95 - 62 - Си в UNIX printi( n, base ){ register int i; if( n < 0 ){ putchar( '-' ); n = -n; } if( i = n / base ) printi( i, base ); i = n % base ; putchar( i >= 10 ? 'A' + i - 10 : '0' + i ); } Попробуйте написать нерекурсивный вариант с накоплением ответа в строке. Приве- дем рекурсивный вариант, накапливающий ответ в строке s и пользующийся аналогом функ- ции printi: функция prints - такая же, как printi, но вместо вызовов putchar(нечто); в ней написаны операторы *res++ = нечто; и рекурсивно вызывается конечно же prints. Итак: static char *res; ... текст функции prints ... char *itos( n, base, s ) char *s; /* указывает на char[] массив для ответа */ { res = s; prints(n, base); *res = '\0'; return s; } main(){ char buf[20]; printf( "%s\n", itos(19,2,buf); } 1.124. Напишите функцию для побитной распечатки целого числа. Имейте в виду, что число содержит 8 * sizeof(int) бит. Указание: используйте операции битового сдвига и &. Ответ: printb(n){ register i; for(i = 8 * sizeof(int) - 1; i >= 0; --i) putchar(n & (1 << i) ? '1':'0'); } 1.125. Напишите функцию, склоняющую существительные русского языка в зависимости от их числа. Например: printf( "%d кирпич%s", n, grammar( n, "ей", "", "а" )); Ответ: char *grammar( i, s1, s2, s3 ) char *s1, /* прочее */ *s2, /* один */ *s3; /* два, три, четыре */ { i = i % 100; if( i > 10 && i <= 20 ) return s1; i = i % 10; if( i == 1 ) return s2; if( i == 2 || i == 3 || i == 4 ) return s3; return s1; } А. Богатырев, 1992-95 - 63 - Си в UNIX 1.126. Напишите оператор printf, печатающий числа из интервала 0..99 с добавлением нуля перед числом, если оно меньше 10 : 00 01 ... 09 10 11 ... Используйте условное выражение, формат. Ответ: printf ("%s%d", n < 10 ? "0" : "", n); либо printf ("%02d", n ); либо printf ("%c%c", '0' + n/10, '0' + n%10 ); 1.127. Предостережем от одной ошибки, часто допускаемой начинающими. putchar( "c" ); является ошибкой. putchar( 'c' ); верно. Дело в том, что putchar требует аргумент - символ, тогда как "c" - СТРОКА из одного символа. Большинство компиляторов (те, которые не проверяют прототипы вызова стан- дартных функций) НЕ обнаружит здесь никакой синтаксической ошибки (кстати, ошибка эта - семантическая). Также ошибочны операторы printf ( '\n' ); /* нужна строка */ putchar( "\n" ); /* нужен символ */ putchar( "ab" ); /* нужен символ */ putchar( 'ab' ); /* ошибка в буквенной константе */ char c; if((c = getchar()) == "q" ) ... ; /* нужно писать 'q' */ Отличайте строку из одного символа и символ - это разные вещи! (Подробнее об этом - в следующей главе). 1.128. Весьма частой является ошибка "промах на единицу", которая встречается в очень многих и разнообразных случаях. Вот одна из возможных ситуаций: int m[20]; int i = 0; while( scanf( "%d", & m[i++] ) != EOF ); printf( "Ввели %d чисел\n", i ); В итоге i окажется на 1 больше, чем ожидалось. Разберитесь в чем дело. Ответ: аргументы функции вычисляются до ее вызова, поэтому когда мы достигаем конца файла и scanf возвращает EOF, i++ в вызове scanf все равно делается. Надо напи- сать while( scanf( "%d", & m[i] ) != EOF ) i++; 1.129. Замечание по стилистике: при выводе сообщения на экран printf( "Hello \n" ); пробелы перед \n достаточно бессмысленны, поскольку на экране никак не отобразятся. Надо писать (экономя память) printf( "Hello\n" ); А. Богатырев, 1992-95 - 64 - Си в UNIX Единственный случай, когда такие пробелы значимы - это когда вы выводите текст инвер- сией. Тогда пробелы отображаются как светлый фон. Еще неприятнее будет printf( "Hello\n " ); поскольку концевые пробелы окажутся в начале следующей строки. 1.130. printf - интерпретирующая функция, т.е. работает она довольно медленно. Поэ- тому вместо char s[20]; int i; ... printf( "%c", s[i] ); и printf( "\n" ); надо всегда писать putchar( s[i] ); и putchar( '\n' ); поскольку printf в конце-концов (сделав все преобразования по формату) внутри себя вызывает putchar. Так сделаем же это сразу! 1.131. То, что параметр "формат" в функции printf может быть выражением, позволяет делать некоторые удобные вещи. Например: int x; ... printf( x ? "значение x=%d\n" : "x равен нулю\n\n", x); Формат здесь - условное выражение. Если x!=0, то будет напечатано значение x по фор- мату %d. Если же x==0, то будет напечатана строка, не содержащая ни одного %-та. В результате аргумент x в списке аргументов будет просто проигнорирован. Однако, нап- ример int x = ... ; printf( x > 30000 ? "%f\n" : "%d\n", x); (чтобы большие x печатались в виде 31000.000000) незаконно, поскольку целое число нельзя печатать по формату %f ни в каких случаях. Единственным способом сделать это является явное приведение x к типу double: printf("%f\n", (double) x); Будет ли законен оператор? printf( x > 30000 ? "%f\n" : "%d\n", x > 30000 ? (double) x : x ); Ответ: нет. Условное выражение для аргумента будет иметь "старший" тип - double. А значение типа double нельзя печатать по формату %d. Мы должны использовать здесь оператор if: if( x > 30000 ) printf("%f\n", (double)x); else printf("%d\n", x); 1.132. Напишите функцию, печатающую размер файла в удобном виде: если файл меньше одного килобайта - печатать его размер в байтах, если же больше - в килобайтах (и мегабайтах). #define KBYTE 1024L /* килобайт */ #define THOUSAND 1024L /* кб. в мегабайте */ А. Богатырев, 1992-95 - 65 - Си в UNIX void tellsize(unsigned long sz){ if(sz < KBYTE) printf("%lu байт", sz); else{ unsigned long Kb = sz/KBYTE; unsigned long Mb = Kb/THOUSAND; unsigned long Dec = ((sz % KBYTE) * 10) / KBYTE; if( Mb ){ Kb %= THOUSAND; printf( Dec ? "%lu.%03lu.%01lu Мб." : "%lu.%lu Мб.", Mb, Kb, Dec ); } else printf( Dec ? "%lu.%01lu Кб.":"%lu Кб.", Kb, Dec); } putchar('\n'); } 1.133. Для печати строк используйте printf("%s", string); /* A */ но не printf(string); /* B */ Если мы используем вариант B, а в строке встретится символ '%' char string[] = "abc%defg"; то %d будет воспринято как формат для вывода целого числа. Во-первых, сама строка %d не будет напечатана; во-вторых - что же будет печататься по этому формату, когда у нас есть лишь единственный аргумент - string?! Напечатается какой-то мусор! 1.134. Почему оператор char s[20]; scanf("%s", s); printf("%s\n", s); в ответ на ввод строки Пушкин А.С. печатает только "Пушкин"? Ответ: потому, что концом текста при вводе по формату %s считается либо \n, либо пробел, либо табуляция, а не только \n; то есть формат %s читает слово из текста. Чтение всех символов до конца строки, (включая пробелы) должно выглядеть так: scanf("%[^\n]\n", s); %[^\n] - читать любые символы, кроме \n (до \n) \n - пропустить \n на конце строки %[abcdef] - читать слово, состоящее из перечисленных букв. %[^abcde] - читать слово из любых букв, кроме перечисленных (прерваться по букве из списка). Пусть теперь строки входной информации имеют формат: Фрейд Зигмунд 1856 1939 Пусть мы хотим считывать в строку s фамилию, в целое y - год рождения, а прочие поля - игнорировать. Как это сделать? Нам поможет формат "подавление присваивания" %*: scanf("%s%*s%d%*[^\n]\n", s, &y ); А. Богатырев, 1992-95 - 66 - Си в UNIX %* пропускает поле по формату, указанному после *, не занося его значение ни в какую переменную, а просто "забывая" его. Так формат "%*[^\n]\n" игнорирует "хвост" строки, включая символ перевода строки. Символы " ", "\t", "\n" в формате вызывают пропуск всех пробелов, табуляций, переводов строк во входном потоке, что можно описать как int c; while((c = getc(stdin))== ' ' || c == '\t' || c == '\n' ); либо как формат %*[ \t\n] Перед числовыми форматами (%d, %o, %u, %ld, %x, %e, %f), а также %s, пропуск пробелов делается автоматически. Поэтому scanf("%d%d", &x, &y); и scanf("%d %d", &x, &y); равноправны (пробел перед вторым %d просто не нужен). Неявный пропуск пробелов не делается перед %c и %[... , поэтому в ответ на ввод строки "12 5 x" пример main(){ int n, m; char c; scanf("%d%d%c", &n, &m, &c); printf("n=%d m=%d c='%c'\n", n, m, c); } напечатает "n=12 m=5 c=' '", то есть в c будет прочитан пробел (предшествовавший x), а не x. Автоматический пропуск пробелов перед %s не позволяет считывать по %s строки, лидирующие пробелы которых должны сохраняться. Чтобы лидирующие пробелы также считы- вались, следует использовать формат scanf("%[^\n]%*1[\n]", s); в котором модификатор длины 1 заставляет игнорировать только один символ \n, а не ВСЕ пробелы и переводы строк, как "\n". К сожалению (как показал эксперимент) этот формат не в состоянии прочесть пустую строку (состоящую только из \n). Поэтому можно сделать глобальный вывод: строки надо считывать при помощи функций gets() и fgets()! 1.135. Еще пара слов про scanf: scanf возвращает число успешно прочитанных им данных (обработанных %-ов) или EOF в конце файла. Неудача может наступить, если данное во входном потоке не соответствует формату, например строка 12 quack для int d1; double f; scanf("%d%lf", &d1, &f); В этом случае scanf прочтет 12 по формату %d в переменную d1, но слово quack не отве- чает формату %lf, поэтому scanf прервет свою работу и выдаст значение 1 (успешно про- чел один формат). Строка quack останется невостребованной - ее прочитают последующие вызовы функций чтения; а сейчас f останется неизмененной. 1.136. Си имеет квалификатор const, указывающий, что значение является не перемен- ной, а константой, и попытка изменить величину по этому имени является ошибкой. Во многих случаях const может заменить #define, при этом еще явно указан тип константы, что полезно для проверок компилятором. А. Богатырев, 1992-95 - 67 - Си в UNIX const int x = 22; x = 33; /* ошибка: константу нельзя менять */ Использование const с указателем: Указуемый объект - константа const char *pc = "abc"; pc[1] = 'x'; /* ошибка */ pc = "123"; /* OK */ Сам указатель - константа char *const cp = "abc"; cp[1] = 'x'; /* OK */ cp = "123"; /* ошибка */ Указуемый объект и сам указатель - константы const char *const cpc = "abc"; cpc[1] = 'x'; /* ошибка */ cpc = "123"; /* ошибка */ Указатель на константу необходимо объявлять как const TYPE* int a = 1; const int b = 2; const int *pca = &a; /* OK, просто рассматриваем a как константу */ const int *pcb = &b; /* OK */ int *pb = &b; /* ошибка, так как тогда возможно было бы написать */ *pb = 3; /* изменить константу b */ 1.137. Стандартная функция быстрой сортировки qsort (алгоритм quick sort) имеет такой формат: чтобы отсортировать массив элементов типа TYPE TYPE arr[N]; надо вызывать qsort(arr,/* Что сортировать? Не с начала: arr+m */ N, /* Сколько первых элементов массива? */ /* можно сортировать только часть: n < N */ sizeof(TYPE),/* Или sizeof arr[0] */ /* размер одного элемента массива*/ cmp); где int cmp(TYPE *a1, TYPE *a2); функция сравнения элементов *a1 и *a2. Ее аргументы - АДРЕСА двух каких-то элементов сортируемого массива. Функцию cmp мы должны написать сами - это функция, задающая упорядочение элементов массива. Для сортировки по возрастанию функция cmp() должна возвращать целое < 0, если *a1 должно идти раньше *a2 < = 0, если *a1 совпадает с *a2 == > 0, если *a1 должно идти после *a2 > Для массива строк элементы массива имеют тип (char *), поэтому аргументы функции имеют тип (char **). Требуемому условию удовлетворяет такая функция: А. Богатырев, 1992-95 - 68 - Си в UNIX char *arr[N]; ... cmps(s1, s2) char **s1, **s2; { return strcmp(*s1, *s2); } (Про strcmp смотри раздел "Массивы и строки"). Заметим, что в некоторых системах программирования (например в TurboC++ |-) вы должны использовать функцию сравнения с прототипом int cmp (const void *a1, const void *a2); и внутри нее явно делать приведение типа: cmps (const void *s1, const void *s2) { return strcmp(*(char **)s1, *(char **)s2); } или можно поступить следующим образом: int cmps(char **s1, char **s2){ return strcmp(*s1, *s2); } typedef int (*CMPS)(const void *, const void *); qsort((void *) array, ..., ..., (CMPS) cmps); Наконец, возможно и просто объявить int cmps(const void *A, const void *B){ return strcmp(A, B); } Для массива целых годится такая функция сравнения: int arr[N]; ... cmpi(i1, i2) int *i1, *i2; { return *i1 - *i2; } Для массива структур, которые мы сортируем по целому полю key, годится struct XXX{ int key; ... } arr[N]; cmpXXX(st1, st2) struct XXX *st1, *st2; { return( st1->key - st2->key ); } Пусть у нас есть массив long. Можно ли использовать long arr[N]; ... cmpl(L1, L2) long *L1, *L2; { return *L1 - *L2; } Ответ: оказывается, что нет. Функция cmpl должна возвращать целое, а разность двух long-ов имеет тип long. Поэтому компилятор приводит эту разность к типу int (как правило обрубанием старших битов). При этом (если long-числа были велики) результат может изменить знак! Например: main(){ int n; long a = 1L; long b = 777777777L; n = a - b; /* должно бы быть отрицательным... */ printf( "%ld %ld %d\n", a, b, n ); } ____________________ |- TurboC - компилятор Си в MS DOS, разработанный фирмой Borland International. А. Богатырев, 1992-95 - 69 - Си в UNIX печатает 1 777777777 3472. Функция сравнения должна выглядеть так: cmpl(L1, L2) long *L1, *L2; { if( *L1 == *L2 ) return 0; if( *L1 < *L2 ) return (-1); return 1; } или cmpl(L1, L2) long *L1, *L2; { return( *L1 == *L2 ? 0 : *L1 < *L2 ? -1 : 1 ); } поскольку важна не величина возвращенного значения, а только ее знак. Учтите, что для использования функции сравнения вы должны либо определить функ- цию сравнения до ее использования в qsort(): int cmp(...){ ... } /* реализация */ ... qsort(..... , cmp); либо предварительно объявить имя функции сравнения, чтобы компилятор понимал, что это именно функция: int cmp(); qsort(..... , cmp); ... int cmp(...){ ... } /* реализация */ 1.138. Пусть у нас есть две программы, пользующиеся одной и той же структурой данных W: a.c b.c -------------------------- ------------------------------ #include <fcntl.h> #include <fcntl.h> struct W{ int x,y; }a; struct W{ int x,y; }b; main(){ int fd; main(){ int fd; a.x = 12; a.y = 77; fd = open("f", O_RDONLY); fd = creat("f", 0644); read(fd, &b, sizeof b); write(fd, &a, sizeof a); close(fd); close(fd); printf("%d %d\n", b.x, b.y); } } Что будет, если мы изменим структуру на struct W { long x,y; }; или struct W { char c; int x,y; }; в файле a.c и забудем сделать это в b.c? Будут ли правильно работать эти программы? Из наблюдаемого можно сделать вывод, что если две или несколько программ (или частей одной программы), размещенные в разных файлах, используют общие - типы данных (typedef); - структуры и объединения; - константы (определения #define); - прототипы функций; то их определения лучше выносить в общий include-файл (header-файл), дабы все прог- раммы придерживались одних и тех же общих соглашений. Даже если эти соглашения со А. Богатырев, 1992-95 - 70 - Си в UNIX временем изменятся, то они изменятся во всех файлах синхронно и как бы сами собой. В нашем случае исправлять определение структуры придется только в include-файле, а не выискивать все места, где оно написано, ведь при этом немудрено какое-нибудь место и пропустить! W.h ----------------------- struct W{ long x, y; }; a.c b.c -------------------------- ------------------ #include <fcntl.h> #include <fcntl.h> #include "W.h" #include "W.h" struct W a; struct W b; main(){ ... main(){ ... printf("%ld... Кроме того, вынесение общих фрагментов текста программы (определений структур, конс- тант, и.т.п.) в отдельный файл экономит наши силы и время - вместо того, чтобы наби- вать один и тот же текст много раз в разных файлах, мы теперь пишем в каждом файле единственную строку - директиву #include. Кроме того, экономится и место на диске, ведь программа стала короче! Файлы включения имеют суффикс .h, что означает "header-file" (файл-заголовок). Синхронную перекомпиляцию всех программ в случае изменения include-файла можно задать в файле Makefile - программе для координатора make|-: all: a b echo Запуск a и b a ; b a: a.c W.h cc a.c -o a b: b.c W.h cc b.c -o b Правила make имеют вид цель: список_целей_от_которых_зависит команда команда описывает что нужно сделать, чтобы изготовить файл цель из файлов список_целей_от_которых_зависит. Команда выполняется только если файл цель еще не существует, либо хоть один из файлов справа от двоеточия является более "молодым" (свежим), чем целевой файл (смотри поле st_mtime и сисвызов stat в главе про UNIX). 1.139. Программа на Си может быть размещена в нескольких файлах. Каждый файл высту- пает в роли "модуля", в котором собраны сходные по назначению функции и переменные. Некоторые переменные и функции можно сделать невидимыми для других модулей. Для этого надо объявить их static: - Объявление переменной внутри функции как static делает переменную статической (т.е. она будет сохранять свое значение при выходе из функции) и ограничивает ее видимость пределами данной функции. - Переменные, описанные вне функций, и так являются статическими (по классу памяти). Однако слово static и в этом случае позволяет управлять видимостью этих переменных - они будут видимы только в пределах данного файла. - Функции, объявленные как static, также видимы только в пределах данного файла. - Аргументы функции и локальные (автоматические) переменные функции и так сущест- вуют только на время вызова данной функции (память для них выделяется в стеке ____________________ |- Подробное описание make смотри в документации по системе UNIX. А. Богатырев, 1992-95 - 71 - Си в UNIX при входе в функцию и уничтожается при выходе) и видимы только внутри ее тела. Аргументы функции нельзя объявлять static: f(x) static x; { x++; } незаконно. Таким образом все переменные и функции в данном файле делятся на две группы: - Видимые только внутри данного файла (локальные для модуля). Такие имена объяв- ляются с использованием ключевого слова static. В частности есть еще "более локальные" переменные - автоматические локалы функций и их формальные аргументы, которые видимы только в пределах данной функции. Также видимы лишь в пределах одной функции статические локальные переменные, объявленные в теле функции со словом static. - Видимые во всех файлах (глобальные имена). Глобальные имена образуют интерфейс модуля и могут быть использованы в других моду- лях. Локальные имена извне модуля недоступны. Если мы используем в файле-модуле функции и переменные, входящие в интерфейс другого файла-модуля, мы должны объявить их как extern ("внешние"). Для функций опи- сатели extern и int можно опускать: // файл A.c int x, y, z; // глобальные char ss[200]; // глоб. static int v, w; // локальные static char *s, p[20]; // лок. int f(){ ... } // глоб. char *g(){ ... } // глоб. static int h(){ ... } // лок. static char *sf(){ ... } // лок. int fi(){ ... } // глоб. // файл B.c extern int x, y; extern z; // int можно опустить extern char ss[]; // размер можно опустить extern int f(); char *g(); // extern можно опустить extern fi(); // int можно опустить Хорошим тоном является написание комментария - из какого модуля или библиотеки импор- тируется переменная или функция: extern int x, y; /* import from A.c */ char *tgetstr(); /* import from termlib */ Следующая программа собирается из файлов A.c и B.c командой|= ____________________ |= Можно задать Makefile вида CFLAGS = -O AB: A.o B.o cc A.o B.o -o AB A.o: A.c cc -c $(CFLAGS) A.c B.o: B.c cc -c $(CFLAGS) B.c и собирать программу просто вызывая команду make. А. Богатырев, 1992-95 - 72 - Си в UNIX cc A.c B.c -o AB Почему компилятор сообщает "x дважды определено"? файл A.c файл B.c ----------------------------------------- int x=12; int x=25; main(){ f(y) int *y; f(&x); { printf("%d\n", x); *y += x; } } Ответ: потому, что в каждом файле описана глобальная переменная x. Надо в одном из них (или в обоих сразу) сделать x локальным именем (исключить его из интерфейса модуля): static int x=...; Почему в следующем примере компилятор сообщает "_f дважды определено"? файл A.c файл B.c ---------------------------------------------------- int x; extern int x; main(){ f(5); g(77); } g(n){ f(x+n); } f(n) { x=n; } f(m){ printf("%d\n", m); } Ответ: надо сделать в файле B.c функцию f локальной: static f(m)... Хоть в одном файле должна быть определена функция main, вызываемая системой при запуске программы. Если такой функции нигде нет - компилятор выдает сообщение "_main неопределено". Функция main должна быть определена один раз! В файле она может нахо- диться в любом месте - не требуется, чтобы она была самой первой (или последней) функцией файла|=. 1.140. В чем ошибка? файл A.c файл B.c ---------------------------------------------------- extern int x; extern int x; main(){ x=2; f(){ f(); printf("%d\n", x); } } Ответ: переменная x в обоих файлах объявлена как extern, в результате память для нее нигде не выделена, т.е. x не определена ни в одном файле. Уберите одно из слов extern! 1.141. В чем ошибка? файл A.c файл B.c ---------------------------------------------------- int x; extern double x; ... ... Типы переменных не совпадают. Большинство компиляторов не ловит такую ошибку, т.к. каждый файл компилируется отдельно, независимо от остальных, а при "склейке" файлов в ____________________ |= Если вы пользуетесь "новым" стилем объявления функций, но не используете прото- типы, то следует определять каждую функцию до первого места ее использования, чтобы компилятору в точке вызова был известен ее заголовок. Это приведет к тому, что main() окажется последней функцией в файле - ее не вызывает никто, зато она вызывает кого-то еще. А. Богатырев, 1992-95 - 73 - Си в UNIX общую выполняемую программу компоновщик знает лишь имена переменных и функций, но не их типы и прототипы. В результате программа нормально скомпилируется и соберется, но результат ее выполнения будет непредсказуем! Поэтому объявления extern тоже полезно выносить в include-файлы: файл proto.h ------------------ extern int x; файл A.c файл B.c ------------------ ------------------ #include "proto.h" #include "proto.h" int x; ... то, что переменная x в A.c оказывается описанной и как extern - вполне допустимо, т.к. в момент настоящего объявления этой переменной это слово начнет просто игнориро- ваться (лишь бы типы в объявлении с extern и без него совпадали - иначе ошибка!). 1.142. Что печатает программа и почему? int a = 1; /* пример Bjarne Stroustrup-а */ void f(){ int b = 1; static int c = 1; printf("a=%d b=%d c=%d\n", a++, b++, c++); } void main(){ while(a < 4) f(); } Ответ: a=1 b=1 c=1 a=2 b=1 c=2 a=3 b=1 c=3 1.143. Автоматическая переменная видима только внутри блока, в котором она описана. Что напечатает программа? /* файл A.c */ int x=666; /*глоб.*/ main(){ f(3); printf(" ::x = %d\n", x); g(2); g(5); printf(" ::x = %d\n", x); } g(n){ static int x=17; /*видима только в g*/ printf("g::x = %2d g::n = %d\n", x++, n); if(n) g(n-1); else x = 0; } /* файл B.c */ extern x; /*глобал*/ f(n){ /*локал функции*/ x++; /*глобал*/ { int x; /*локал блока*/ x = n+1; /*локал*/ А. Богатырев, 1992-95 - 74 - Си в UNIX n = 2*x; /*локал*/ } x = n-1; /*глобал*/ } 1.144. Функция, которая - не содержит внутри себя статических переменных, хранящих состояние процесса обработки данных (функция без "памяти"); - получает значения параметров только через свои аргументы (но не через глобальные статические переменные); - возвращает значения только через аргументы, либо как значение функции (через return); называется реентерабельной (повторно входимой) или чистой (pure). Такая функция может параллельно (или псевдопараллельно) использоваться несколькими "потоками" обра- ботки информации в нашей программе, без какого-либо непредвиденного влияния этих "потоков обработки" друг на друга. Первый пункт требований позволяет функции не зависеть ни от какого конкретного процесса обработки данных, т.к. она не "помнит" обработанных ею ранее данных и не строит свое поведение в зависимости от них. Вторые два пункта - это требование, чтобы все без исключения пути передачи данных в функцию и из нее (интерфейс функции) были перечислены в ее заголовке. Это лишает функцию "побочных эффектов", не предусмотренных программистом при ее вызове (программист обычно смотрит только на заголовок функции, и не выискивает "тайные" связи функции с программой через глобальные переменные, если только это специально не оговорено). Вот пример не реентерабельной функции: FILE *fp; ... /* глобальный аргумент */ char delayedInput () { static char prevchar; /* память */ char c; c = prevchar; prevchar = getc (fp); return c; } А вот ее реентерабельный эквивалент: char delayedInput (char *prevchar, FILE *fp) { char c; c = *prevchar; *prevchar = getc (fp); return c; } /* вызов: */ FILE *fp1, *fp2; char prev1, prev2, c1, c2; ... x1 = delayedInput (&prev1, fp1); x2 = delayedInput (&prev2, fp2); ... Как видим, все "запоминающие" переменные (т.е. prevchar) вынесены из самой функции и подаются в нее в виде аргумента. Реентерабельные функции независимы от остальной части программы (их можно скопи- ровать в другой программный проект без изменений), более понятны (поскольку все зат- рагиваемые ими внешние переменные перечислены как аргументы, не надо выискивать в теле функции глобальных переменных, передающих значение в/из функции, т.е. эта функ- ция не имеет побочных влияний), более надежны (хотя бы потому, что компилятор в сос- тоянии проверить прототип такой функции и предупредить вас, если вы забыли задать какой-то аргумент; если же аргументы передаются через глобальные переменные - вы можете забыть проинициализировать какую-то из них). Старайтесь делать функции реен- терабельными! А. Богатырев, 1992-95 - 75 - Си в UNIX Вот еще один пример на эту тему. Не-реентерабельный вариант: int x, y, result; int f (){ static int z = 4; y = x + z; z = y - 1; return x/2; } Вызов: x=13; result = f(); printf("%d\n", y); А вот реентерабельный эквивалент: int y, result, zmem = 4; int f (/*IN*/ int x, /*OUT*/ int *ay, /*INOUT*/ int *az){ *az = (*ay = x + *az) - 1; return x/2; } Вызов: result = f(13, &y, &zmem); printf("%d\n", y); 1.145. То, что формат заголовка функции должен быть известен компилятору до момента ее использования, побуждает нас помещать определение функции до точки ее вызова. Так, если main вызывает f, а f вызывает g, то в файле функции расположатся в порядке g() { } f() { ... g(); ... } main(){ ... f(); ... } Программа обычно разрабатывается "сверху-вниз" - от main к деталям. Си же вынуждает нас размещать функции в программе в обратном порядке, и в итоге программа читается снизу-вверх - от деталей к main, и читать ее следует от конца файла к началу! Так мы вынуждены писать, чтобы удовлетворить Си-компилятор: #include <stdio.h> unsigned long g(unsigned char *s){ const int BITS = (sizeof(long) * 8); unsigned long sum = 0; for(;*s; s++){ sum ^= *s; /* cyclic rotate left */ sum = (sum<<1)|(sum>>(BITS-1)); } return sum; } void f(char *s){ printf("%s %lu\n", s, g((unsigned char *)s)); } int main(int ac, char *av[]){ int i; for(i=1; i < ac; i++) f(av[i]); return 0; } А вот как мы разрабатываем программу: А. Богатырев, 1992-95 - 76 - Си в UNIX #include <stdio.h> int main(int ac, char *av[]){ int i; for(i=1; i < ac; i++) f(av[i]); return 0; } void f(char *s){ printf("%s %lu\n", s, g((unsigned char *)s)); } unsigned long g(unsigned char *s){ const int BITS = (sizeof(long) * 8); unsigned long sum = 0; for(;*s; s++){ sum ^= *s; /* cyclic rotate left */ sum = (sum<<1)|(sum>>(BITS-1)); } return sum; } и вот какую ругань производит Си-компилятор в ответ на эту программу: "0000.c", line 10: identifier redeclared: f current : function(pointer to char) returning void previous: function() returning int : "0000.c", line 7 "0000.c", line 13: identifier redeclared: g current : function(pointer to uchar) returning ulong previous: function() returning int : "0000.c", line 11 Решением проблемы является - задать прототипы (объявления заголовков) всех функций в начале файла (или даже вынести их в header-файл). #include <stdio.h> int main(int ac, char *av[]); void f(char *s); unsigned long g(unsigned char *s); ... Тогда функции будет можно располагать в тексте в любом порядке. 1.146. Рассмотрим процесс сборки программы из нескольких файлов на языке Си. Пусть мы имеем файлы file1.c, file2.c, file3.c (один из них должен содержать среди других функций функцию main). Ключ компилятора -o заставляет создавать выполняемую прог- рамму с именем, указанным после этого ключа. Если этот ключ не задан - будет создан выполняемый файл a.out cc file1.c file2.c file3.c -o file Мы получили выполняемую программу file. Это эквивалентно 4-м командам: cc -c file1.c получится file1.o cc -c file2.c file2.o cc -c file3.c file3.o cc file1.o file2.o file3.o -o file Ключ -c заставляет компилятор превратить файл на языке Си в "объектный" файл А. Богатырев, 1992-95 - 77 - Си в UNIX (содержащий машинные команды; не будем вдаваться в подробности). Четвертая команда "склеивает" объектные файлы в единое целое - выполняемую программу|-. При этом, если какие-то функции, используемые в нашей программе, не были определены (т.е. спрограм- мированы нами) ни в одном из наших файлов - будет просмотрена библиотека стандартных функций. Если же каких-то функций не окажется и там - будет выдано сообщение об ошибке. Если у нас уже есть какие-то готовые объектные файлы, мы можем транслировать только новые Си-файлы: cc -c file4.c cc file1.o file2.o file3.o file4.o -o file или (что то же самое, но cc сам разберется, что надо делать) cc file1.o file2.o file3.o file4.c -o file Существующие у нас объектные файлы с отлаженными функциями удобно собрать в библио- теку - файл специальной структуры, содержащий все указанные файлы (все файлы склеены в один длинный файл, разделяясь специальными заголовками, см. include-файл <ar.h>): ar r file.a file1.o file2.o file3.o Будет создана библиотека file.a, содержащая перечисленные .o файлы (имена библиотек в UNIX имеют суффикс .a - от слова archive, архив). После этого можно использовать библиотеку: cc file4.o file5.o file.a -o file Механизм таков: если в файлах file4.o и file5.o не определена какая-то функция (функ- ции), то просматривается библиотека, и в список файлов для "склейки" добавляется файл из библиотеки, содержащий определение этой функции (из библиотеки он не удаляется!). Тонкость: из библиотеки берутся не ВСЕ файлы, а лишь те, которые содержат определения недостающих функций|=. Если, в свою очередь, файлы, извлекаемые из библиотеки, будут содержать неопределенные функции - библиотека (библиотеки) будут просмотрены еще раз и.т.д. (на самом деле достаточно максимум двух проходов, так как при первом просмотре библиотеки можно составить ее каталог: где какие функции в ней содержатся и кого вызывают). Можно указывать и несколько библиотек: cc file6.c file7.o \ file.a mylib.a /lib/libLIBR1.a -o file Таким образом, в команде cc можно смешивать имена файлов: исходных текстов на Си .c, объектных файлов .o и файлов-библиотек .a. Просмотр библиотек, находящихся в стандартных местах (каталогах /lib и /usr/lib), можно включить и еще одним способом: указав ключ -l. Если библиотека называется /lib/libLIBR1.a или /usr/lib/libLIBR2.a то подключение делается ключами -lLIBR1 и -lLIBR2 ____________________ |- На самом деле, для "склейки" объектных файлов в выполняемую программу, команда /bin/cc вызывает программу /bin/ld - link editor, linker, редактор связей, компонов- щик. |= Поэтому библиотека может быть очень большой, а к нашей программе "приклеится" лишь небольшое число файлов из нее. В связи с этим стремятся делать файлы, помещаемые в библиотеку, как можно меньше: 1 функция; либо "пачка" функций, вызывающих друг друга. А. Богатырев, 1992-95 - 78 - Си в UNIX соответственно. cc file1.c file2.c file3.o mylib.a -lLIBR1 -o file Список библиотек и ключей -l должен идти после имен всех исходных .c и объектных .o файлов. Библиотека стандартных функций языка Си /lib/libc.a (ключ -lc) подключается автоматически ("подключить" библиотеку - значит вынудить компилятор просматривать ее при сборке, если какие-то функции, использованные вами, не были вами определены), то есть просматривается всегда (именно эта библиотека содержит коды, например, для printf, strcat, read). Многие прикладные пакеты функций поставляются именно в виде библиотек. Такие библиотеки состоят из ряда .o файлов, содержащих объектные коды для различных функций (т.е. функции в скомпилированном виде). Исходные тексты от большинства библиотек не поставляются (так как являются коммерческой тайной). Тем не менее, вы можете исполь- зовать эти функции, так как вам предоставляются разработчиком: - описание (документация). - include-файлы, содержащие форматы данных используемые функциями библиотеки (именно эти файлы включались #include в исходные тексты библ. функций. Теперь уже вы должны включать их в свою программу). Таким образом вы знаете, как надо вызывать библиотечные функции и какие структуры данных вы должны использовать в своей программе для обращения к ним (хотя и не имеете текстов самих библиотечных функций, т.е. не знаете, как они устроены. Например, вы часто используете printf(), но задумываетесь ли вы о ее внутреннем устройстве?). Некоторые библиотечные функции могут быть вообще написаны не на Си, а на ассемблере или другом языке программирования|-|-. Еще раз обращаю ваше внимание, что библиотека содержит не исходные тексты функций, а скомпилированные коды (и include-файлы содер- жат (как правило) не тексты функций, а только описание форматов данных)! Библиотека может также содержать статические данные, вроде массивов строк-сообщений об ошибках. Посмотреть список файлов, содержащихся в библиотеке, можно командой ar tv имяФайлаБиблиотеки а список имен функций - командой nm имяФайлаБиблиотеки Извлечь файл (файлы) из архива (скопировать его в текущий каталог), либо удалить его из библиотеки можно командами ar x имяФайлаБиблиотеки имяФайла1 ... ar d имяФайлаБиблиотеки имяФайла1 ... где ... означает список имен файлов. "Лицом" библиотек служат прилагаемые к ним include-файлы. Системные include- файлы, содержащие общие форматы данных для стандартных библиотечных функций, хранятся в каталоге /usr/include и подключаются так: для /usr/include/файл.h надо #include <файл.h> для /usr/include/sys/файл.h #include <sys/файл.h> ____________________ |-|- Обратите внимание, что библиотечные функции не являются частью ЯЗЫКА Си как та- кового. То, что в других языках (PL/1, Algol-68, Pascal) является частью языка (встроено в язык)- в Си вынесено на уровень библиотек. Например, в Си нет оператора вывода; функция вывода printf - это библиотечная функция (хотя и общепринятая). Та- ким образом мощь языка Си состоит именно в том, что он позволяет использовать функ- ции, написанные другими программистами и даже на других языках, т.е. является функци- онально расширяемым. А. Богатырев, 1992-95 - 79 - Си в UNIX (sys - это каталог, где описаны форматы данных, используемых ядром ОС и системными вызовами). Ваши собственные include-файлы (посмотрите в предыдущий раздел!) ищутся в текущем каталоге и включаются при помощи #include "файл.h" /* ./файл.h */ #include "../h/файл.h" /* ../h/файл.h */ #include "/usr/my/файл.h" /* /usr/my/файл.h */ Непременно изучите содержимое стандартных include-файлов в своей системе! В качестве резюме - схема, поясняющая "превращения" Си-программы из текста на языке программирования в выполняемый код: все файлы .c могут использовать общие include-файлы; их подстановку в текст, а также обработку #define произведет препро- цессор cpp file1.c file2.c file3.c | | | "препроцессор" | cpp | cpp | cpp | | | "компиляция" | cc -c | cc -c | cc -c | | | file1.o file2.o file3.o | | | -----------*----------- | Неявно добавятся: ld |<----- /lib/libc.a (библ. станд. функций) | /lib/crt0.o (стартер) "связывание" | "компоновка" |<----- Явно указанные библиотеки: | -lm /lib/libm.a V a.out 1.147. Напоследок - простой, но жизненно важный совет. Если вы пишете программу, которую вставите в систему для частого использования, поместите в исходный текст этой программы идентификационную строку наподобие static char id[] = "This is /usr/abs/mybin/xprogram"; Тогда в случае аварии в файловой системе, если вдруг ваш файл "потеряется" (то есть у него пропадет имя - например из-за порчи каталога), то он будет найден программой проверки файловой системы - fsck - и помещен в каталог /lost+found под специальным кодовым именем, ничего общего не имеющим со старым. Чтобы понять, что это был за файл и во что его следует переименовать (чтобы восстановить правильное имя), мы при- меним команду strings имя_файла Эта команда покажет все длинные строки из печатных символов, содержащиеся в данном файле, в частности и нашу строку id[]. Увидев ее, мы сразу поймем, что файл надо переименовать так: mv имя_файла /usr/abs/mybin/xprogram 1.148. Где размещать include-файлы и как программа узнает, где же они лежат? Стан- дартные системные include-файлы размещены в /usr/include и подкаталогах. Если мы пишем некую свою программу (проект) и используем директивы #include "имяФайла.h" А. Богатырев, 1992-95 - 80 - Си в UNIX то обычно include-файлы имяФайла.h лежат в текущем каталоге (там же, где и файлы с программой на Си). Однако мы можем помещать ВСЕ наши include-файлы в одно место (скажем, известное группе программистов, работающих над одним и тем же проектом). Хорошее место для всех ваших личных include-файлов - каталог (вами созданный) $HOME/include где $HOME - ваш домашний каталог. Хорошее место для общих include-файлов - каталог /usr/local/include Как сказать компилятору, что #include "" файлы надо брать из определенного места, а не из текущего каталога? Это делает ключ компилятора cc -Iимя_каталога ... Например: /* Файл x.c */ #include "x.h" int main(int ac, char *av[]){ .... return 0; } И файл x.h находится в каталоге /home/abs/include/x.h (/home/abs - мой домашний ката- лог). Запуск программы на компиляцию выглядит так: cc -I/home/abs/include -O x.c -o x или cc -I$HOME/include -O x.c -o x Или, если моя программа x.c находится в /home/abs/progs cc -I../include -O x.c -o x Ключ -O задает вызов компилятора с оптимизацией. Ключ -I оказывает влияние и на #include <> директивы тоже. Для ОС Solaris на машинах Sun программы для оконной системы X Window System содержат строки вроде #include <X11/Xlib.h> #include <X11/Xutil.h> На Sun эти файлы находятся не в /usr/include/X11, а в /usr/openwin/include/X11. Поэ- тому запуск на компиляцию оконных программ на Sun выглядит так: cc -O -I/usr/openwin/include xprogram.c \ -o xprogram -L/usr/openwin/lib -lX11 где -lX11 задает подключение графической оконной библиотеки Xlib. Если include-файлы находятся во многих каталогах, то можно задать поиск в нес- кольких каталогах, к примеру: cc -I/usr/openwin/include -I/usr/local/include -I$HOME/include ... А. Богатырев, 1992-95 - 81 - Си в UNIX 2. Массивы, строки, указатели. Массив представляет собой агрегат из нескольких переменных одного и того же типа. Массив с именем a из LENGTH элементов типа TYPE объявляется так: TYPE a[LENGTH]; Это соответствует тому, что объявляются переменные типа TYPE со специальными именами a[0], a[1], ..., a[LENGTH-1]. Каждый элемент массива имеет свой номер - индекс. Доступ к x-ому элементу массива осуществляется при помощи операции индексации: int x = ... ; /* целочисленный индекс */ TYPE value = a[x]; /* чтение x-ого элемента */ a[x] = value; /* запись в x-тый элемент */ В качестве индекса может использоваться любое выражение, выдающее значение целого типа: char, short, int, long. Индексы элементов массива в Си начинаются с 0 (а не с 1), и индекс последнего элемента массива из LENGTH элементов - это LENGTH-1 (а не LENGTH). Поэтому цикл по всем элементам массива - это TYPE a[LENGTH]; int indx; for(indx=0; indx < LENGTH; indx++) ...a[indx]...; indx < LENGTH равнозначно indx <= LENGTH-1. Выход за границы массива (попытка чтения/записи несуществующего элемента) может привести к непредсказуемым результатам и поведению программы. Отметим, что это одна из самых распространенных ошибок. Статические массивы можно объявлять с инициализацией, перечисляя значения их элементов в {} через запятую. Если задано меньше элементов, чем длина массива - остальные элементы считаются нулями: int a10[10] = { 1, 2, 3, 4 }; /* и 6 нулей */ Если при описании массива с инициализацией не указать его размер, он будет подсчитан компилятором: int a3[] = { 1, 2, 3 }; /* как бы a3[3] */ В большинстве современных компьютеров (с фон-Неймановской архитектурой) память представляет собой массив байт. Когда мы описываем некоторую переменную или массив, в памяти выделяется непрерывная область для хранения этой переменной. Все байты памяти компьютера пронумерованы. Номер байта, с которого начинается в памяти наша переменная, называется адресом этой переменной (адрес может иметь и более сложную структуру, чем просто целое число - например состоять из номера сегмента памяти и номера байта в этом сегменте). В Си адрес переменной можно получить с помощью опера- ции взятия адреса &. Пусть у нас есть переменная var, тогда &var - ее адрес. Адрес нельзя присваивать целой переменной; для хранения адресов используются указатели (смотри ниже). Данное может занимать несколько подряд идущих байт. Размер в байтах участка памяти, требуемого для хранения значения типа TYPE, можно узнать при помощи операции sizeof(TYPE), а размер переменной - при помощи sizeof(var). Всегда выполняется sizeof(char)==1. В некоторых машинах адреса переменных (а также агрегатов данных - массивов и структур) кратны sizeof(int) или sizeof(double) - это так называемое "выравнивание (alignment) данных на границу типа int". Это позволяет делать доступ к данным более быстрым (аппаратура работает эффективнее). Язык Си предоставляет нам средство для работы с адресами данных - указатели (pointer)|-. Указатель физически - это адрес некоторой переменной ("указуемой" пере- менной). Отличие указателей от машинных адресов состоит в том, что указатель может содержать адреса данных только определенного типа. Указатель ptr, который может ука- зывать на данные типа TYPE, описывается так: TYPE var; /* переменная */ TYPE *ptr; /* объявление ук-ля */ ptr = & var; А. Богатырев, 1992-95 - 82 - Си в UNIX В данном случае мы занесли в указательную переменную ptr адрес переменной var. Будем говорить, что указатель ptr указывает на переменную var (или, что ptr установлен на var). Пусть TYPE равно int, и у нас есть массив и указатели: int array[LENGTH], value; int *ptr, *ptr1; Установим указатель на x-ый элемент массива ptr = & array[x]; Указателю можно присвоить значение другого указателя на такой же тип. В результате оба указателя будут указывать на одно и то же место в памяти: ptr1 = ptr; Мы можем изменять указуемую переменную при помощи операции * *ptr = 128; /* занести 128 в указуемую перем. */ value = *ptr; /* прочесть указуемую переменную */ В данном случае мы заносим и затем читаем значение переменной array[x], на которую поставлен указатель, то есть *ptr означает сейчас array[x] Таким образом, операция * (значение по адресу) оказывается обратной к операции & (взятие адреса): & (*ptr) == ptr и * (&value) == value Операция * объясняет смысл описания TYPE *ptr; оно означает, что значение выражения *ptr будет иметь тип TYPE. Название же типа самого указателя - это (TYPE *). В част- ности, TYPE может сам быть указательным типом - можно объявить указатель на указа- тель, вроде char **ptrptr; Имя массива - это константа, представляющая собой указатель на 0-ой элемент мас- сива. Этот указатель отличается от обычных тем, что его нельзя изменить (установить на другую переменную), поскольку он сам хранится не в переменной, а является просто некоторым постоянным адресом. массив указатель ____________ _____ array: | array[0] | ptr:| * | | array[1] | | | array[2] |<--------- сейчас равен &array[2] | ... | Следствием такой интерпретации имен массивов является то, что для того чтобы поста- вить указатель на начало массива, надо писать ptr = array; или ptr = &array[0]; но не ptr = &array; Операция & перед одиноким именем массива не нужна и недопустима! Такое родство указателей и массивов позволяет нам применять операцию * к имени массива: value = *array; означает то же самое, что и value = array[0]; Указатели - не целые числа! Хотя физически это и номера байтов, адресная ариф- метика отличается от обычной. Так, если дан указатель TYPE *ptr; и номер байта (адрес), на который указывает ptr, равен byteaddr, то ptr = ptr + n; /* n - целое, может быть и < 0 */ заставит ptr указывать не на байт номер byteaddr + n, а на байт номер А. Богатырев, 1992-95 - 83 - Си в UNIX byteaddr + (n * sizeof(TYPE)) то есть прибавление единицы к указателю продвигает адрес не на 1 байт, а на размер указываемого указателем типа данных! Пусть указатель ptr указывает на x-ый элемент массива array. Тогда после TYPE *ptr2 = array + L; /* L - целое */ TYPE *ptr1 = ptr + N; /* N - целое */ ptr += M; /* M - целое */ указатели указывают на ptr1 == &array[x+N] и ptr == &array[x+M] ptr2 == &array[L] Если мы теперь рассмотрим цепочку равенств *ptr2 = *(array + L) = *(&array[L]) = array[L] то получим ОСНОВНОЕ ПРАВИЛО: пусть ptr - указатель или имя массива. Тогда операции индексации, взятия значения по адресу, взятия адреса и прибавления целого к указателю связаны соотношениями: ptr[x] тождественно *(ptr+x) &ptr[x] тождественно ptr+x (тождества верны в обе стороны), в том числе при x==0 и x < 0. Так что, например, ptr[-1] означает *(ptr-1) ptr[0] означает *ptr Указатели можно индексировать подобно массивам. Рассмотрим пример: /* индекс: 0 1 2 3 4 */ double numbers[5] = { 0.0, 1.0, 2.0, 3.0, 4.0 }; double *dptr = &numbers[2]; double number = dptr[2]; /* равно 4.0 */ numbers: [0] [1] [2] [3] [4] | [-2] [-1] [0] [1] [2] dptr поскольку если dptr = &numbers[x] = numbers + x то dptr[i] = *(dptr + i) = = *(numbers + x + i) = numbers[x + i] Указатель на один тип можно преобразовать в указатель на другой тип: такое пре- образование не вызывает генерации каких-либо машинных команд, но заставляет компиля- тор изменить параметры адресной арифметики, а также операции выборки данного по ука- зателю (собственно, разница в указателях на данные разных типов состоит только в раз- мерах указуемых типов; а также в генерации команд `->' для выборки полей структур, если указатель - на структурный тип). Целые (int или long) числа иногда можно преобразовывать в указатели. Этим поль- зуются при написании драйверов устройств для доступа к регистрам по физическим адре- сам, например: А. Богатырев, 1992-95 - 84 - Си в UNIX unsigned short *KISA5 = (unsigned short *) 0172352; Здесь возникают два тонких момента: 1. Как уже было сказано, адреса данных часто выравниваются на границу некоторого типа. Мы же можем задать невыровненное целое значение. Такой адрес будет некорректен. 2. Структура адреса, поддерживаемая процессором, может не соответствовать формату целых (или длинных целых) чисел. Так обстоит дело с IBM PC 8086/80286, где адрес состоит из пары short int чисел, хранящихся в памяти подряд. Однако весь адрес (если рассматривать эти два числа как одно длинное целое) не является обычным long-числом, а вычисляется более сложным способом: адресная пара SEGMENT:OFFSET преобразуется так unsigned short SEGMENT, OFFSET; /*16 бит: [0..65535]*/ unsigned long ADDRESS = (SEGMENT << 4) + OFFSET; получается 20-и битный физический адрес ADDRESS Более того, на машинах с диспетчером памяти, адрес, хранимый в указателе, явля- ется "виртуальным" (т.е. воображаемым, ненастоящим) и может не совпадать с физи- ческим адресом, по которому данные хранятся в памяти компьютера. В памяти может одновременно находиться несколько программ, в каждой из них будет своя система адресации ("адресное пространство"), отсчитывающая виртуальные адреса с нуля от начала области памяти, выделенной данной программе. Преобразование виртуальных адресов в физические выполняется аппаратно. В Си принято соглашение, что указатель (TYPE *)0 означает "указатель ни на что". Он является просто признаком, используемым для обозначения несуществующего адреса или конца цепочки указателей, и имеет специальное обозначение NULL. Обращение (выборка или запись данных) по этому указателю считается некорректным (кроме случая, когда вы пишете машинно-зависимую программу и работаете с физическими адресами). Отметим, что указатель можно направить в неправильное место - на участок памяти, содержащий данные не того типа, который задан в описании указателя; либо вообще содержащий неизвестно что: int i = 2, *iptr = &i; double x = 12.76; iptr += 7; /* куда же он указал ?! */ iptr = (int *) &x; i = *iptr; Само присваивание указателю некорректного значения еще не является ошибкой. Ошибка возникнет лишь при обращении к данным по этому указателю (такие ошибки довольно тяжело искать!). При передаче имени массива в качестве параметра функции, как аргумент передается не копия САМОГО МАССИВА (это заняло бы слишком много места), а копия АДРЕСА 0-ого элемента этого массива (т.е. указатель на начало массива). f(int x ){ x++; } g(int xa[]){ xa[0]++; } int a[2] = { 1, 1 }; /* объявление с инициализацией */ main(){ f(a[0]); printf("%d\n",a[0]); /* a[0] осталось равно 1*/ g(a ); printf("%d\n",a[0]); /* a[0] стало равно 2 */ } В f() в качестве аргумента передается копия элемента a[0] (и изменение этой копии не приводит к изменению самого массива - аргумент x является локальной переменной в f()), а в g() таким локалом является АДРЕС массива a - но не сам массив, поэтому xa[0]++ изменяет сам массив a (зато, например, xa++ внутри g() изменило бы лишь локальную указательную переменную xa, но не адрес массива a). Заметьте, что поскольку массив передается как указатель на его начало, то размер массива в объявлении аргумента можно не указывать. Это позволяет одной функцией А. Богатырев, 1992-95 - 85 - Си в UNIX обрабатывать массивы разной длины: вместо Fun(int xa[5]) { ... } можно Fun(int xa[] ) { ... } или даже Fun(int *xa ) { ... } Если функция должна знать длину массива - передавайте ее как дополнительный аргумент: int sum( int a[], int len ){ int s=0, i; for(i=0; i < len; i++) s += a[i]; return( s ); } ... int arr[10] = { ... }; ... int sum10 = sum(arr, 10); ... Количество элементов в массиве TYPE arr[N]; можно вычислить специальным образом, как #define LENGTH (sizeof(arr) / sizeof(arr[0])) или #define LENGTH (sizeof(arr) / sizeof(TYPE)) Оба способа выдадут число, равное N. Эти конструкции обычно употребляются для вычис- ления длины массивов, задаваемых в виде TYPE arr[] = { ....... }; без явного указания размера. sizeof(arr) выдает размер всего массива в байтах. sizeof(arr[0]) выдает размер одного элемента. И все это не зависит от типа элемента (просто потому, что все элементы массивов имеют одинаковый размер). Строка в Си - это последовательность байт (букв, символов, литер, character), завершающаяся в конце специальным признаком - байтом '\0'. Этот признак добавляется компилятором автоматически, когда мы задаем строку в виде "строка". Длина строки (т.е. число литер, предшествующих '\0') нигде явно не хранится. Длина строки ограни- чена лишь размером массива, в котором сохранена строка, и может изменяться в процессе работы программы в пределах от 0 до длины массива-1. При передаче строки в качестве аргумента в функцию, функции не требуется знать длину строки, т.к. передается указа- тель на начало массива, а наличие ограничителя '\0' позволяет обнаружить конец строки при ее просмотре. С массивами байт можно использовать следующую конструкцию, задающую массивы (строки) одинакового размера: char stringA [ITSSIZE]; char stringB [sizeof stringA]; В данном разделе мы в основном будем рассматривать строки и указатели на символы. 2.1. Операции взятия адреса объекта и разыменования указателя - взаимно обратны. TYPE objx; TYPE *ptrx = &objx; /* инициализируем адресом objx */ *(&objx) = objx; &(*ptrx) = ptrx; Вот пример того, как можно заменить условный оператор условным выражением (это удастся не всегда): if(c) a = 1; else b = 1; А. Богатырев, 1992-95 - 86 - Си в UNIX Предупреждение: такой стиль не способствует понятности программы и даже компактности ее кода. #include <stdio.h> int main(int ac, char *av[]){ int a, b, c; a = b = c = 0; if(av[1]) c = atoi(av[1]); *(c ? &a : &b) = 1; /* !!! */ printf("cond=%d a=%d b=%d\n", c, a, b); return 0; } 2.2. Каким образом инициализируются по умолчанию внешние и статические массивы? Ини- циализируются ли по умолчанию автоматические массивы? Каким образом можно присваи- вать значения элементам массива, относящегося к любому классу памяти? 2.3. Пусть задан массив int arr[10]; что тогда означают выражения: arr[0] *arr *arr + 2 arr[2] *(arr + 2) arr &arr[2] arr+2 2.4. Правильно ли написано увеличение величины, на которую указывает указатель a, на единицу? *a++; Ответ: нет, надо: (*a)++; или *a += 1; 2.5. Дан фрагмент текста: char a[] = "xyz"; char *b = a + 1; Чему равны b[-1] b[2] "abcd"[3] (Ответ: 'x', '\0', 'd' ) Можно ли написать a++ ? То же про b++ ? Можно ли написать b=a ? a=b ? (нет, да, да, нет) 2.6. Ниже приведена программа, вычисляющая среднее значение элементов массива int arr [] = {1, 7, 4, 45, 31, 20, 57, 11}; main () { int i; long sum; for ( i = 0, sum = 0L; i < (sizeof(arr)/sizeof(int)); i++ ) sum += arr[i]; printf ("Среднее значение = %ld\n", sum/8) А. Богатырев, 1992-95 - 87 - Си в UNIX } Перепишите указанную программу с применением указателей. 2.7. Что напечатается в результате работы программы? char arr[] = {'С', 'Л', 'А', 'В', 'А'}; main () { char *pt; int i; pt = arr + sizeof(arr) - 1; for( i = 0; i < 5; i++, pt-- ) printf("%c %c\n", arr[i], *pt); } Почему массив arr[] описан вне функции main()? Как внести его в функцию main() ? Ответ: написать внутри main static char arr[]=... 2.8. Можно ли писать на Си так: f( n, m ){ int x[n]; int y[n*2]; int z[n * m]; ... } Ответ: к сожалению нельзя (Си - это не Algol). При отведении памяти для массива в качестве размера должна быть указана константа или выражение, которое может быть еще во время компиляции вычислено до целочисленной константы, т.е. массивы имеют фиксиро- ванную длину. 2.9. Предположим, что у нас есть описание массива static int mas[30][100]; a) выразите адрес mas[22][56] иначе b) выразите адрес mas[22][0] двумя способами c) выразите адрес mas[0][0] тремя способами 2.10. Составьте программу инициализации двумерного массива a[10][10], выборки эле- ментов с a[5][5] до a[9][9] и их распечатки. Используйте доступ к элементам по ука- зателю. 2.11. Составьте функцию вычисления скалярного произведения двух векторов. Длина векторов задается в качестве одного из аргументов. 2.12. Составьте функцию умножения двумерных матриц a[][] * b[][]. 2.13. Составьте функцию умножения трехмерных матриц a[][][] * b[][][]. 2.14. Для тех, кто программировал на языке Pascal: какая допущена ошибка? char a[10][20]; char c; int x,y; ... c = a[x,y]; Ответ: многомерные массивы в Си надо индексировать так: А. Богатырев, 1992-95 - 88 - Си в UNIX c = a[x][y]; В написанном же примере мы имеем в качестве индекса выражение x,y (оператор "запя- тая") со значением y, т.е. c = a[y]; Синтаксической ошибки нет, но смысл совершенно изменился! 2.15. Двумерные массивы в памяти представляются как одномерные. Например, если int a[N][M]; то конструкция a[y][x] превращается при компиляции в одномерную конструкцию, подобную такой: int a[N * M]; /* массив развернут построчно */ #define a_yx(y, x) a[(x) + (y) * M] то есть a[y][x] есть *(&a[0][0] + y * M + x) Следствием этого является то, что компилятор для генерации индексации двумерных (и более) массовов должен знать M - размер массива по 2-ому измерению (а также 3-ему, 4-ому, и.т.д.). В частности, при передаче многомерного массива в функцию f(arr) int arr[N][M]; { ... } /* годится */ f(arr) int arr[] [M]; { ... } /* годится */ f(arr) int arr[] []; { ... } /* не годится */ f(arr) int (*arr)[M]; { ... } /* годится */ f(arr) int *arr [M]; { ... } /* не годится: это уже не двумерный массив, а одномерный массив указателей */ А также при описании внешних массивов: extern int a[N][M]; /* годится */ extern int a[ ][M]; /* годится */ extern int a[ ][ ]; /* не годится: компилятор не сможет сгенерить операцию индексации */ Вот как, к примеру, должна выглядеть работа с двумерным массивом arr[ROWS][COLS], отведенным при помощи malloc(); void f(int array[][COLS]){ int x, y; for(y=0; y < ROWS; y++) for(x=0; x < COLS; x++) array[y][x] = 1; } void main(){ int *ptr = (int *) malloc(sizeof(int) * ROWS * COLS); f( (int (*) [COLS]) ptr); } 2.16. Как описывать ссылки (указатели) на двумерные массивы? Рассмотрим такую прог- рамму: А. Богатырев, 1992-95 - 89 - Си в UNIX #include <stdio.h> #define First 3 #define Second 5 char arr[First][Second] = { "ABC.", { 'D', 'E', 'F', '?', '\0' }, { 'G', 'H', 'Z', '!', '\0' } }; char (*ptr)[Second]; main(){ int i; ptr = arr; /* arr и ptr теперь взаимозаменимы */ for(i=0; i < First; i++) printf("%s\t%s\t%c\n", arr[i], ptr[i], ptr[i][2]); } Указателем здесь является ptr. Отметим, что у него задана размерность по второму измерению: Second, именно для того, чтобы компилятор мог правильно вычислить двумер- ные индексы. Попробуйте сами объявить char (*ptr)[4]; char (*ptr)[6]; char **ptr; и увидеть, к каким невеселым эффектам это приведет (компилятор, кстати, будет ругаться; но есть вероятность, что он все же странслирует это для вас. Но работать оно будет плачевно). Попробуйте также использовать ptr[x][y]. Обратите также внимание на инициализацию строк в нашем примере. Строка "ABC." равносильна объявлению { 'A', 'B', 'C', '.', '\0' }, 2.17. Массив s моделирует двумерный массив char s[H][W]; Перепишите пример при помощи указателей, избавьтесь от операции умножения. Прямоугольник (x0,y0,width,height) лежит целиком внутри (0,0,W,H). char s[W*H]; int x,y; int x0,y0,width,height; for(x=0; x < W*H; x++) s[x] = '.'; ... for(y=y0; y < y0+height; y++) for(x=x0; x < x0+width; x++) s[x + W*y] = '*'; Ответ: char s[W*H]; int i,j; int x0,y0,width,height; char *curs; ... for(curs = s + x0 + W*y0, i=0; i < height; i++, curs += W-width) for(j=0; j < width; j++) *curs++ = '*'; Такая оптимизация возможна в некоторых функциях из главы "Работа с видеопамятью". А. Богатырев, 1992-95 - 90 -