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" пример
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, при этом еще явно указан тип константы,
что полезно для проверок компилятором.
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? Будут ли правильно работать эти программы?
Из наблюдаемого можно сделать вывод, что если две или несколько программ (или
частей одной программы), размещенные в разных файлах, используют общие
- типы данных (typedef);
- структуры и объединения;
- константы (определения #define);
- прототипы функций;
то их определения лучше выносить в общий
include-
файл (
header-
файл), дабы все программы придерживались одних и тех же общих соглашений. Даже если эти соглашения со
временем изменятся, то они изменятся во всех файлах синхронно и как бы сами собой. В нашем случае исправлять определение структуры придется только в 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, также видимы только в пределах данного файла.
- Аргументы функции и локальные (автоматические) переменные функции и так существуют только на время вызова данной функции (память для них выделяется в стеке при входе в функцию и уничтожается при выходе) и видимы только внутри ее тела.
Аргументы функции нельзя объявлять 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 командой
***
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
Назад | Содержание | Вперед