of(); 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) вынесены из самой функции и
подаются в нее в виде аргумента.
Реентерабельные функции независимы от остальной части программы (их можно скопи-
ровать в другой программный проект без изменений), более понятны (поскольку все зат-
рагиваемые ими внешние переменные перечислены как аргументы, не надо вы