Страница:
будет выдавать (x+y)/2. Если мы напишем так:
int msort(x, y) int x, y;
{ int tmp;
if(x > y){ tmp=x; x=y; y=tmp; }
return (x+y)/2;
}
int x=20, y=8;
main(){
msort(x,y); printf("%d %d\n", x, y); /* 20 8 */
}
то мы не достигнем желаемого эффекта. Здесь переставляются x и y, которые являются
локальными переменными, т.е. копиями фактических параметров. Поэтому вне функции эта
перестановка никак не проявляется!
Чтобы мы могли изменить аргументы, копироваться в локальные переменные должны не
сами значения аргументов, а их адреса:
int msort(xptr, yptr) int *xptr, *yptr;
{ int tmp;
if(*xptr > *yptr){tmp= *xptr;*xptr= *yptr;*yptr=tmp;}
return (*xptr + *yptr)/2;
}
int x=20, y=8, z;
main(){
z = msort(&x,&y);
printf("%d %d %d\n", x, y, z); /* 8 20 14 */
}
Обратите внимание, что теперь мы передаем в функцию не значения x и y, а их адреса &x
и &y.
Именно поэтому (чтобы x смог измениться) стандартная функция scanf() требует
указания адресов:
int x; scanf("%d", &x); /* но не scanf("%d", x); */
Заметим, что адрес от арифметического выражения или от константы (а не от переменной)
вычислить нельзя, поэтому законны:
int xx=12, *xxptr = &xx, a[2] = { 13, 17 };
int *fy(){ return &y; }
msort(&x, &a[0]); msort(a+1, xxptr);
msort(fy(), xxptr);
но незаконны
msort(&(x+1), &y); и msort(&x, &17);
Заметим еще, что при работе с адресами мы можем направить указатель в неверное место
и получить непредсказуемые результаты:
msort(&xx - 20, a+40);
(указатели указывают неизвестно на что).
Резюме: если аргумент служит только для передачи значения В функцию - его не
надо (хотя и можно) делать указателем на переменную, содержащую требуемое значение
(если только это уже не указатель). Если же аргумент служит для передачи значения ИЗ
функции - он должен быть указателем на переменную возвращаемого типа (лучше
А. Богатырев, 1992-95 - 49 - Си в UNIX
возвращать значение как значение функции - return-ом, но иногда надо возвращать нес-
колько значений - и этого главного "окошка" не хватает).
Контрольный вопрос: что печатает фрагмент?
int a=2, b=13, c;
int f(x, y, z) int x, *y, z;
{
*y += x; x *= *y; z--;
return (x + z - a);
}
main(){ c=f(a, &b, a+4); printf("%d %d %d\n",a,b,c); }
(Ответ: 2 15 33)
1.102. Формальные аргументы функции - это такие же локальные переменные. Параметры
как бы описаны в самом внешнем блоке функции:
char *func1(char *s){
int s; /* ошибка: повторное определение имени s */
...
}
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. Напишите функцию, печатающую целое число в системе счисления с основанием
int msort(x, y) int x, y;
{ int tmp;
if(x > y){ tmp=x; x=y; y=tmp; }
return (x+y)/2;
}
int x=20, y=8;
main(){
msort(x,y); printf("%d %d\n", x, y); /* 20 8 */
}
то мы не достигнем желаемого эффекта. Здесь переставляются x и y, которые являются
локальными переменными, т.е. копиями фактических параметров. Поэтому вне функции эта
перестановка никак не проявляется!
Чтобы мы могли изменить аргументы, копироваться в локальные переменные должны не
сами значения аргументов, а их адреса:
int msort(xptr, yptr) int *xptr, *yptr;
{ int tmp;
if(*xptr > *yptr){tmp= *xptr;*xptr= *yptr;*yptr=tmp;}
return (*xptr + *yptr)/2;
}
int x=20, y=8, z;
main(){
z = msort(&x,&y);
printf("%d %d %d\n", x, y, z); /* 8 20 14 */
}
Обратите внимание, что теперь мы передаем в функцию не значения x и y, а их адреса &x
и &y.
Именно поэтому (чтобы x смог измениться) стандартная функция scanf() требует
указания адресов:
int x; scanf("%d", &x); /* но не scanf("%d", x); */
Заметим, что адрес от арифметического выражения или от константы (а не от переменной)
вычислить нельзя, поэтому законны:
int xx=12, *xxptr = &xx, a[2] = { 13, 17 };
int *fy(){ return &y; }
msort(&x, &a[0]); msort(a+1, xxptr);
msort(fy(), xxptr);
но незаконны
msort(&(x+1), &y); и msort(&x, &17);
Заметим еще, что при работе с адресами мы можем направить указатель в неверное место
и получить непредсказуемые результаты:
msort(&xx - 20, a+40);
(указатели указывают неизвестно на что).
Резюме: если аргумент служит только для передачи значения В функцию - его не
надо (хотя и можно) делать указателем на переменную, содержащую требуемое значение
(если только это уже не указатель). Если же аргумент служит для передачи значения ИЗ
функции - он должен быть указателем на переменную возвращаемого типа (лучше
А. Богатырев, 1992-95 - 49 - Си в UNIX
возвращать значение как значение функции - return-ом, но иногда надо возвращать нес-
колько значений - и этого главного "окошка" не хватает).
Контрольный вопрос: что печатает фрагмент?
int a=2, b=13, c;
int f(x, y, z) int x, *y, z;
{
*y += x; x *= *y; z--;
return (x + z - a);
}
main(){ c=f(a, &b, a+4); printf("%d %d %d\n",a,b,c); }
(Ответ: 2 15 33)
1.102. Формальные аргументы функции - это такие же локальные переменные. Параметры
как бы описаны в самом внешнем блоке функции:
char *func1(char *s){
int s; /* ошибка: повторное определение имени s */
...
}
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. Напишите функцию, печатающую целое число в системе счисления с основанием