int x = 12; f(x){ int y = x*x; if(x) f(x - 1); } main(){ int x=173, z=21; f(2); }Локальные переменные и аргументы функции отводятся в стеке при вызове функции и уничтожаются при выходе из нее:
-+ +- вершина стека |локал y=0 | |аргумент x=0 | f(0) |---------------|-------- "кадр" |локал y=1 | frame |аргумент x=1 | f(1) |---------------|-------- |локал y=4 | |аргумент x=2 | f(2) |---------------|-------- |локал z=21 | auto: |локал x=173 | main() ================================== дно стека static: глобал x=12 ==================================
Автоматические локальные переменные и аргументы функции видимы только в том вызове функции, в котором они отведены; но не видимы ни в вызывающих, ни в вызываемых функциях (т.е. видимость их ограничена рамками своего "кадра" стека). Статические глобальные переменные видимы в любом кадре, если только они не "перекрыты" (заслонены) одноименной локальной переменной (или формалом) в данном кадре.
Что напечатает программа? Постарайтесь ответить на этот вопрос не выполняя программу на машине!
x1 x2 x3 x4 x5 int x = 12; /* x1 */ | . . . . f(){ |___ . . . int x = 8; /* x2, перекрытие */ : | . . . printf( "f: x=%d\n", x ); /* x2 */ : | . . . x++; /* x2 */ : | . . . } :--+ . . . g(x){ /* x3 */ :______ . . printf( "g: x=%d\n", x ); /* x3 */ : | . . x++; /* x3 */ : | . . } :-----+ . . h(){ :_________ . int x = 4; /* x4 */ : | . g(x); /* x4 */ : |___ { int x = 55; } /* x5 */ : : | printf( "h: x=%d\n", x ); /* x4 */ : |--+ } :--------+ main(){ | f(); h(); | printf( "main: x=%d\n", x ); /* x1 */ | } ---Ответ:
f: x=8 g: x=4 h: x=4 main: x=12
Обратите внимание на функцию g. Аргументы функции служат копиями фактических параметров (т.е. являются локальными переменными функции, проинициализированными значениями фактических параметров), поэтому их изменение не приводит к изменению фактического параметра. Чтобы изменять фактический параметр, надо передавать его адрес!
Поясним последнюю фразу. (Внимание! Возможно, что данный пункт вам следует читать ПОСЛЕ главы про указатели). Пусть мы хотим написать функцию, которая обменивает свои аргументы 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);(указатели указывают неизвестно на что).
Резюме: если аргумент служит только для передачи значения В функцию - его не надо (хотя и можно) делать указателем на переменную, содержащую требуемое значение (если только это уже не указатель). Если же аргумент служит для передачи значения ИЗ функции - он должен быть указателем на переменную возвращаемого типа (лучше возвращать значение как значение функции - 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)
Формальные аргументы функции - это такие же локальные переменные. Параметры как бы описаны в самом внешнем блоке функции:
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; ... }Мораль такова: формальные аргументы можно смело изменять и использовать как локальные переменные.
Два последних типа параметров должны быть указателями. Иногда (особенно в прототипах и в документации) бывает полезно указывать класс параметра в виде комментария:
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 */ }
Это полезно потому, что иногда трудно понять - зачем параметр описан как указатель. То ли по нему выдается из функции информация, то ли это просто указатель на данные (массив), передаваемые в функцию. В первом случае указуемые данные будут изменены, а во втором - нет. В первом случае указатель должен указывать на зарезервированную нами область памяти, в которой будет размещен результат. Пример на эту тему есть в главе "Текстовая обработка" (функция bi_conv).
void func( int arg1 , char *arg2 /* argument 2 */ , char *arg3[] , time_t time_stamp ){ ... }
Суть его в том, что запятые пишутся в столбик и в одну линию с ( и ) скобками для аргументов. При таком стиле легче добавлять и удалять аргументы, чем при версии с запятой в конце. Этот же стиль применим, например, к перечислимым типам:
enum { red , green , blue };Напишите программу, форматирующую заголовки функций таким образом.
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)); }
main() { double y; int x = 12; y = sin (x); printf ("%s\n", y); }Ответ:
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();
y = sin( (double) x ); и sin(12.0); вместо sin(12);
Первых двух проблем в современном Си удается избежать благодаря заданию прототипов функций (о них подробно рассказано ниже, в конце главы "Текстовая обработка"). Например, sin имеет прототип
double sin(double x);Третяя проблема (ошибка в формате) не может быть локализована средствами Си и имеет более-менее приемлемое решение лишь в языке C++ (streams).
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.
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 (т.к. приоритет у присваивания выше, чем у запятой). Обратите внимание, что выражение без присваивания (которое может вообще не иметь эффекта или иметь только побочный эффект) вполне законно:
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;
switch(c){ case 1: x++; break; case 2: y++; break; defalt: z++; break; }Если c=3, то z++ не происходит. Почему? (Потому, что defalt: - это метка, а не ключевое слово default).
* Для трансляции программы, использующей стандартные математические функции sin,
cos, exp, log, sqrt, и.т.п. следует задавать ключ компилятора -lm
cc file.c -o file -lm
** Слово extern ("внешняя") не является обязательным, но является признаком хорошего тона - вы сообщаете программисту, читающему эту программу, что данная функция реализована в другом файле, либо вообще является стандартной и берется из библиотеки.
© Copyright А. Богатырев, 1992-95
Си в UNIX
Назад | Содержание | Вперед