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)); }то же в варианте 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. Ответ: 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; } 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" ); Единственный случай, когда такие пробелы значимы - это когда вы выводите текст инверсией. Тогда пробелы отображаются как светлый фон. Еще неприятнее будет 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 /* кб. в мегабайте */ 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 );%* пропускает поле по формату, указанному после *, не занося его значение ни в какую переменную, а просто "забывая" его. Так формат "%*[^\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" пример
Автоматический пропуск пробелов перед %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, при этом еще явно указан тип константы, что полезно для проверок компилятором. 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 **). Требуемому условию удовлетворяет такая функция: 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 ); }печатает 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? Будут ли правильно работать эти программы? Из наблюдаемого можно сделать вывод, что если две или несколько программ (или частей одной программы), размещенные в разных файлах, используют общие
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: f(x) static x; { x++; }незаконно. Таким образом все переменные и функции в данном файле делятся на две группы:
Если мы используем в файле-модуле функции и переменные, входящие в интерфейс другого файла-модуля, мы должны объявить их как 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 командой*** 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. 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 должна быть определена один раз! В файле она может находиться в любом месте - не требуется, чтобы она была самой первой (или последней) функцией файла****. * TurboC - компилятор Си в MS DOS, разработанный фирмой Borland International. ** Подробное описание make смотри в документации по системе UNIX. *** Можно задать Makefile вида **** Если вы пользуетесь "новым" стилем объявления функций, но не используете прототипы, то следует определять каждую функцию до первого места ее использования, чтобы компилятору в точке вызова был известен ее заголовок. Это приведет к тому, что main() окажется последней функцией в файле - ее не вызывает никто, зато она вызывает кого-то еще. [Назад][Содержание][Вперед] |