Главная | Новости | Статьи | Книги | Ссылки |
Что напечатает программа? (Пример посвящен указателям на функции и массивам функций):
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) ); }
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)); } }
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); }
Напишите функцию, печатающую целое число в системе счисления с основанием 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); }
Напишите функцию для побитной распечатки целого числа. Имейте в виду, что число содержит 8 * sizeof(int) бит. Указание: используйте операции битового сдвига и &. Ответ:
printb(n){ register i; for(i = 8 * sizeof(int) - 1; i >= 0; --i) putchar(n & (1 << i) ? '1':'0'); }
Напишите функцию, склоняющую существительные русского языка в зависимости от их числа. Например:
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; }
Напишите оператор 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 );
putchar( "c" ); является ошибкой. putchar( 'c' ); верно.
Дело в том, что putchar требует аргумент - символ, тогда как "c" - СТРОКА из одного символа. Большинство компиляторов (те, которые не проверяют прототипы вызова стандартных функций) НЕ обнаружит здесь никакой синтаксической ошибки (кстати, ошибка эта - семантическая). Также ошибочны операторы
printf ( '\n' ); /* нужна строка */ putchar( "\n" ); /* нужен символ */ putchar( "ab" ); /* нужен символ */ putchar( 'ab' ); /* ошибка в буквенной константе */ char c; if((c = getchar()) == "q" ) ... ; /* нужно писать 'q' */Отличайте строку из одного символа и символ - это разные вещи! (Подробнее об этом в следующей главе).
Весьма частой является ошибка "промах на единицу", которая встречается в очень многих и разнообразных случаях. Вот одна из возможных ситуаций:
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++;
printf( "Hello \n" );пробелы перед \n достаточно бессмысленны, поскольку на экране никак не отобразятся. Надо писать (экономя память)
printf( "Hello\n" );
Единственный случай, когда такие пробелы значимы - это когда вы выводите текст инверсией. Тогда пробелы отображаются как светлый фон.
Еще неприятнее будет
printf( "Hello\n " );поскольку концевые пробелы окажутся в начале следующей строки.
char s[20]; int i; ... printf( "%c", s[i] ); и printf( "\n" );надо всегда писать
putchar( s[i] ); и putchar( '\n' );
поскольку printf в конце-концов (сделав все преобразования по формату) внутри себя вызывает putchar. Так сделаем же это сразу!
То, что параметр "формат" в функции 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);
Напишите функцию, печатающую размер файла в удобном виде: если файл меньше одного килобайта - печатать его размер в байтах, если же больше - в килобайтах (и мегабайтах).
#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'); }
printf("%s", string); /* A */ но не printf(string); /* B */Если мы используем вариант B, а в строке встретится символ '%'
char string[] = "abc%defg";
то %d будет воспринято как формат для вывода целого числа. Во-первых, сама строка %d не будет напечатана; во-вторых - что же будет печататься по этому формату, когда у нас есть лишь единственный аргумент - string?! Напечатается какой-то мусор!
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" пример
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()!
Еще пара слов про scanf: scanf возвращает число успешно прочитанных им данных (обработанных %-ов) или EOF в конце файла. Неудача может наступить, если данное во входном потоке не соответствует формату, например строка
12 quackдля
int d1; double f; scanf("%d%lf", &d1, &f);
В этом случае scanf прочтет 12 по формату %d в переменную d1, но слово quack не отвечает формату %lf, поэтому scanf прервет свою работу и выдаст значение 1 (успешно прочел один формат). Строка quack останется невостребованной - ее прочитают последующие вызовы функций чтения; а сейчас f останется неизмененной.
Си имеет квалификатор 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 */
Стандартная функция быстрой сортировки 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(...){ ... } /* реализация */
Пусть у нас есть две программы, пользующиеся одной и той же структурой данных 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).
Программа на Си может быть размещена в нескольких файлах. Каждый файл выступает в роли "модуля", в котором собраны сходные по назначению функции и переменные. Некоторые переменные и функции можно сделать невидимыми для других модулей. Для этого надо объявить их 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() окажется последней функцией в файле - ее не вызывает никто, зато она вызывает кого-то еще.
© Copyright А. Богатырев, 1992-95
Си в UNIX
Назад | Содержание | Вперед