кого параметра. Чтобы изменять фактический параметр, надо передавать его адрес!
А. Богатырев, 1992-95 - 48 - Си в UNIX
1.101. Поясним последнюю фразу. (Внимание! Возможно, что данный пункт вам следует
читать ПОСЛЕ главы про указатели). Пусть мы хотим написать функцию, которая обмени-
вает свои аргументы x и y так, чтобы выполнялось x < y. В качестве значения функция
будет выдавать (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
вместо
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. Кажущаяся
двойственность возникает из-за того, что аргументы функции тоже перечисляются через