ФУНКЦИИ
СТРОИТЕЛЬНЫЕ БЛОКИ ПРОГРАММЫ
СВЯЗЬ МЕЖДУ ФУНКЦИЯМИ: АРГУМЕНТЫ, УКАЗАТЕЛИ, ВОЗВРАТ ЗНАЧЕНИЯ
ТИПЫ ФУНКЦИЙ
Принципы программирования на языке Си основаны на понятии функции. В представленных ранее примерах программирования мы уже воспользовались несколькими функциями: printf( ), scanf( ), getchar( ), putchar( ) и strlen( ). Эти функции являются системными, однако мы создали и несколько своих собственных функций под общим именем main( ). Выполнение программы всегда начинается с команд, содержащихся в функции main( ), затем последняя вызывает другие функции, например getchar( ). Теперь мы переходим к вопросу о том, как создавать свои собственные функции н делать их доступными для функции main( ), а также друг для друга.
Во-первых, что такое функция? Функция - самостоятельная единица программы, спроектированная для реализации конкретной задачи. Функции в языке Си играют ту же роль, какую играют функции, подпрограммы и процедуры в других языках, хотя детали их структуры могут быть разными. Вызов функции приводит к выполнению некоторых действии. Например, при обращении к функции printf( ) осуществляется вывод данных на экран. Другие же функции позволяют получать некоторую величину, используемую затем в программе. К примеру, функция strlen( ) "сообщает" программе длину конкретной строки. В общем функции могут выполнять действия и получать значения величин, используемых в программе.
Почему мы пользуемся функциями? Во-первых, они избавляют нас от повторного программирования. Если конкретную задачу необходимо выполнить в программе несколько раз, мы напишем соответствующую функцию только один раз, а затем будем вызывать ее всегда, когда это требуется. Во-вторых, мы можем применять одну функцию, например putchar( ), в различных программах. Даже в том случае, если некоторая задача выполняется только в одной программе, лучше оформить ее решение в виде функции, поскольку функции повышают уровень модульности программы и, следовательно, облегчают ее чтение, внесение изменений и коррекцию ошибок. Предположим, например, что мы хотим написать программу, которая делает следующее: вводит набор чисел сортирует, эти числа, находит их среднее, выводит на печать гистограмму.
Соответствующую программу можно записать так:
main( )
{
float list [50];
readlist(list);
sort(list);
average(list);
bargrapli(list);
}
Разумеется, мы должны были бы запрограммировать четыре функции readlist( ), sort( ), average( ) и bargraph( ), но... это уже детали. Используя смысловые имена функции, мы четко определяем, что программа делает и как она организована. После этого можно заниматься каждой функцией отдельно и совершенствовать ее до тех пор, пока она не будет правильно выполнять требуемую задачу. Дополнительное преимущество указанного подхода заключается в том, что если мы создадим функции достаточно общего вида, то их можно будет использовать и в других программах.
Многие программисты предпочитают думать о функции, как о "черном ящике"; они задают ее через поступающую информацию (вход) и полученные результаты (выход). Все, что происходит внутри черного ящика, их не касается до тех пор, пока не нужно писать программу, реализующую эту функцию. Когда мы используем, например, функцию printf( ), мы знаем, что должны передать ей управляющую строку и возможно, несколько аргументов. Мы знаем также результат вы зова функций printf( ). He нужно полагать, что при программировании вам придется заниматься созданием функции printf( ). Использование функций указанным выше способом позволяет сконцентрировать внимание на обшей структуре программы, а не на деталях.
Что нам требуется знать о функциях? Нужно знать, как их можно определять, как к ним обращаться и как устанавливать связи между функцией и программой, ее вызывающей. Чтобы изучить это, мы рассмотрим очень простои пример, а затем будем обобщать его, вводя дополнительные характеристики до тех пор, пока не получим полную и ясную картину.
Наша первая скромная цель - создание функции, которая печатает 65 символов * в ряд. Чтобы эта функция выполнялась в некотором контексте, мы включили ее в программу, которая печатает простой титул фирменного бланка. Ниже приведена полная соответствующая программа. Она состоит из функции main( ) и starbar( ).
/* титул фирменного бланка! */
#define NAME "MEGATHINK, INC."
#define ADDRESS "10 Megabuck Plaza"
#define PLACE "Megapolis, CA 94904"
main( )
{
starbar( );
printf("%s\n", NAME);
printf(" %s\n", ADDRESS);
printf("%s\n", PLACE);
starbar( );
}
/* далее следует функция starbar( ) */
#include
#define LIMIT 65
starbar( );
{
int count;
for (count = 1; count <= LIMIT; count++)
putchar('*');
putchar('\n');
}
Результат работы программы выглядит так:
***********************************************************
MEGATHINK, INC 10 Megabuck Plaza Megapolis, CA 94904
***********************************************************
При рассмотрении этой программы необходимо обратить внимание на следующие моменты:
1. Мы вызвали функцию starbar( ) (или, можно сказать, обратились к ней) из функции main( ), используя только ее имя. Это несколько напоминает заклинание, вызывающее злого духа, но, вместо того чтобы чертить пятиугольник, мы помещаем вслед за именем функции точку с запятой, создавая таким образом оператор: starbar( );
РИС. 9.1. Схема выполнения операторов программы титул "фирменною бланка 1".
Это одна из форм вызова функции, но далеко не единственная. Когда в процессе выполнения программы компьютер достигает оператора starbar( ), он находит указанную функцию, после чего начинает выполнять соответствующие ей команды. Затем управление возвращается следующей строке "вызывающе" программы" - в данном случае main( ).
2. При написании функции starbar( ) мы следовали тем же правилам, что и при написании main( ): вначале указывается имя, затем идет открывающая фигурная скобка, приводится описание используемых переменных, даются операторы, определяющие работу функции, и, наконец, закрывающая фигурная скобка. Мы даже поместили перед описанием функции starbar( ) директивы #define и #include, требующиеся для нее, а не для функции main( ).
РИС. 9.2. Структура простой функции.
3. Мы включили функции starbar() и main() в один файл. Вообще говоря, можно было создать два отдельных файла. Один файл несколько упрощает компиляцию, а два отдельных файла облегчают использование одной функции в разных программах. Случай двух и более файлов мы обсудим позже, а пока будем держать все наши функции в одном месте. Закрывающая фигурная скобка функции main( ) указывает компилятору на се конец. Круглые скобки в имени starbar( ) говорят о том, что starbar( ) - это функция. Обратите внимание, что здесь за именем starbar( ) не следует символ "точка с запятой"; его отсутствие служит указанием компилятору, что мы определяем функцию starbar( ), а не используем ее.
Если рассматривать функцию starbar( ) как черный ящик, то ее выход - это напечатанная строка, состоящая из символов *. Какие бы то ни было данные на входе у нее отсутствуют, потому что eй не нужно использовать информацию из вызывающей программы. Вообще, этой функции не требуется связь с вызывающей программой. Обратимся к случаю, когда такая связь необходима.
Титул фирменного бланка выглядел бы несколько лучше, если бы текст был сдвинут к центру. Мы сможем поместить текст в центре, если напечатаем нужное число пробелов перед выводом требуемой строки. Воспользуемся некоторой функцией для печати пробелов. Наша функция space( ) (давайте назовем ее так) будет очень напоминать функцию starbar( ), за исключением того, что на этот раз между функцией main( ) и функцией space( ) должна быть установлена связь, так как необходимо сообщить последней функции о требуемом числе пробелов.
Рассмотрим это более конкретно. В строке, состоящей из звездочек, 65 символов, а в строке MEGATHINK, INC. - 15. Поэтому в нашем первом варианте программы вслед за этим сообщением шло 50 пробелов. Чтобы сместить текст к центру, нужно сначала напечатать 25 пробелов, а потом текст, в результате чего слева и справа от данной фразы окажется по 25 пробелов. Следовательно, необходимо иметь возможность передать величину "25" функции, печатающей пробелы. Мы применяем тот же способ, что и при передаче символа '*' функции putchar( ): используем аргумент. Тогда запись space(25) будет означать, что необходимо напечатать 25 пробелов. 25 - это аргумент. Мы будем вызывать функцию space( ) три раза: один раз для каждой строки адреса. Вот как выглядит эта программа:
/* титул фирменного бланка2 */
#define NAME "MEGATHINK, INC."
#define ADDRESS "10 Megabuck Plaza"
#define PLACE "Mcgapolis, CA 94904"
main( )
{
int spaces;
starbar( );
space(25); /* space( ) использует в качестве аргумента константу*/
printf("%s\n", NAME);
spaces = (65 - strlen(ADDRESS))/2;
/* мы заставляем программу вычислять, сколько пропустить пробелов */
space(spaces); /* аргументом является переменная */
printf("%s\n", ADDRESS);
space((65 - strlen(PLACE))/2); /* аргументом является выражение */
printf(" %s \n", PLACE);
starbar( );
}
/* определение функции starbar( ) */
#include
#define LIMIT 65
starbar( )
{
int count;
for (count = 1;count <= LIMIT;count++) putchar('*');
putchar('\n');
}
/* определение функции space( ) */
space(number)
int number; /* аргумент описывается перед фигурной скобкой */
{
int count /* дополнительная переменная описывается
после фигурной скобки */
for (count = 1;count <= number;count++)
putchar(' ');
}
РИС. 9.3. Программа, печатающая титул фирменного бланка.
Обратите внимание на то, как мы экспериментировали при вы зовах функции space( ): мы задавали аргумент тремя различными способами. Являются ли все они работоспособными? Да - и вот доказательство.
*********************************************************************
MEGATHINK, INC.
10 Megabuck Plaza
Megapolis, CA 94904
*********************************************************************
Рассмотрим сначала, как определить функцию с одним аргументом, после чего перейдем к вопросу о том, как она используется.
Определение нашей функции начинается с двух строк:
space(number)
int number;
Первая строка информирует компилятор о том, что у функции space( ) имеется аргумент и что его имя number. Вторая строка - описание, указывающее компилятору, что аргумент number имеет тип int. Обратите внимание: аргумент описывается перед фигурной скобкой, которая отмечает начало тела функции. Вообще говоря, вы можете объединить эти две строки в одну:
space(int number;)
Независимо от формы записи переменная number называется "формальным" аргументом. Фактически это новая переменная, и в памяти компьютера для нее должна быть выделена отдельная ячейка. Посмотрим, как можно пользоваться этой функцией.
Задача в данном случае состоит в том, чтобы присвоить некоторую величину формальному аргументу number. После того как эта переменная получит свое значение, программа сможет выполнить свою задачу. Мы присваиваем переменной number значение фактического аргумента при вызове функции. Рассмотрим наш первый случай использования функции space( ):
space(25);
Фактический аргумент здесь 25, и эта величина присваивается формальному аргументу - переменной number, т. е. вызов функции оказывает следующее действие:
number = 25;
Короче говоря, формальный аргумент - переменная в вызываемой программе, а фактический аргумент - конкретное значение, присвоенное этой переменной вызывающей программой. Как было показано в нашем примере, фактический аргумент может быть константой, переменной или даже более сложным выражением. Независимо от типа фактического аргумента он вначале вычисляется, а затем его величина (в данном случае некоторое целое число) передается функции. Рассмотрим, например, наше последнее обращение к функции space( ).
space((65- strlen(PLACE))/2);
Рассматривая функцию space( ) как черный ящик, можно сказать, что ее вход - это число пропущенных позиций, а выход -j фактический пропуск позиций. Вход связан с функцией через apгyмент.
РИС. 9.4. Фактические аргументы и формальные аргументы.
С помощью аргумента обеспечивается связь между функциями main( ) и space( ). В то же время переменная count описана в теле функции, и другие функции ничего не знают о ней. Указанная переменная является частью механизма, скрытого внутри черного ящика. Это не та же переменная, что count в starbar().
Если для связи с некоторой функцией требуется более одного аргумента, то наряду с именем функции можно задавать список аргументов, разделенных запятыми, как показано ниже.
printnum(i,j) int i, j;
{ printf(" Новых точек = %d. Всего точек = %d.\n", i, j); }
Мы уже знаем, как передавать информацию из вызывающей программы в вызываемую функцию. Можно ли передавать информацию каким-нибудь другим способом? Этот вопрос послужит нам темой следующего обсуждения.
Создадим функцию, вычисляющую абсолютную величину числа. Абсолютная величина числа - это его значение (если отбросить знак). Следовательно, абсолютная величина 5 равна 5, а абсолютная величина -3 равна 3. Мы назовем эту функцию abs( ). Входом для abs() может быть любое число, для которого мы хотим найти абсолютную величину. Выходом функции будет соответствующее неотрицательное число. Входная величина может обрабатываться благодаря наличию аргумента; выходная величина возвращается (т. е. выдается), как вы увидите ниже, при помощи ключевого слова языка Си - return. Поскольку функция abs( ) должна быть вызвана другой функцией, мы создадим простую программу main( ), основной целью которой будет проверка, работает ли функция abs( ). Программа, спроектированная для того, чтобы проверять работу функции именно таким образом, называется "драйвером". Драйвер подвергает функцию последовательным проверкам. Если результаты оказываются удовлетворительными, то ее можно поместить в программу, заслуживающую большего внимания. (Термин "драйвер" обычно относится к программам, управляющим работой устройств.) Приведем далее наш драйвер и функцию, вычисляющую абсолютную величину числа:
/* abs.драйвер */
main( )
{
int а = 10, b = 0, с = -22;
int d, e, f;
d = abs(a);
с= abs(b);
f = abs(c);
printf(" °%d %d %d\n" , d, e, f);
}
/* функция, вычисляющая величину числа */
abs(x) int x;
{
int y;
у = (x < 0) ? -x : x; /* вспомните операцию ?: */
return (у ); /* возвращает значение у вызывающей программе */
}
Результат работы программы выглядит так:
10 0 22
Сначала вспомним операцию условия ?:. Эта операция в функции abs( ) выполняется следующим образом: если x меньше 0, у полагается равным -x; в противном случае у полагается равным x. Это как раз то, что нам нужно, поскольку если x равен -5, то у равен -(-5), т. e. 5.
Ключевое слово return указывает на то, что значение выражения, заключенного в круглые скобки, будет присвоено функции, содержащей это ключевое слово. Поэтому, когда функция abs( ) впервые вызывается нашим драйвером, значением abs(a) будет число 10, которое затем присваивается переменной d.
Переменная у является внутренним объектом функции abs(), но значение у передается в вызывающую программу с помощью оператора return. Действие, оказываемое оператором
d = abs(a);
по-другому можно выразить так:
abs(a);
d = у;
Можно ли в действительности воспользоваться такой записью? Нет, так как вызывающая программа даже не подозревает о том, что переменная у существует.
Возвращаемое значение можно присвоить переменной, как в нашем примере, или использовать как часть некоторого выражения, например, следующим образом:
answer = 2*abs(z) + 25;
printf(" %d\n" , abs(-32 + answer));
Оператор return оказывает и другое действие. Он завершает выполнение функции и передает управление следующему оператору в вызывающей функции. Это происходит даже в том случае, если оператор return является не последним оператором тела функции. Следовательно, функцию abs( ) мы могли бы записать следующим образом:
/* функция, вычисляющая абсолютную величину числа,
вторая версия */
abs(x) int x;
{
if(x < 0)
return(-x);
else
relurn(x);
}
Эта версия программы проще, и в ней не используется дополнительная переменная у. Для пользователя, однако, обе версии неразличимы, поскольку у них имеется один и тот же вход и они обеспечивают один и тот же выход. Только внутренние структуры обеих функций различны. Даже версия данной программы, приведенная ниже, работает точно так же:
/* функция, вычисляющая абсолютную величину числа,
третья версия */
abs(x) int(x);
{
if (x < 0)
return(-x);
else
return(x);
printf(" Профессор Флеппард - болван. \n");
}
Наличие оператора return препятствует тому, чтобы оператор печати printf( ) когда-нибудь выполнился в программе. Профессор Флеппард может пользоваться в своих программах объектным кодом, полученным в результате компиляции данной функции, и никогда не узнает об истинных чувствах своего студента-программиста.
Вы можете также использовать просто оператор return;
Его применение приводит к тому, что функция, в которой он coдержится, завершает свое выполнение и управление возвращается в вызывающую функцию. Поскольку у данного оператора отсутствует выражение в скобках, никакое значение при этом не передается функции.
Мы уже несколько раз касались вопроса о том, что переменные в функции являются ее внутренними переменными и "не известны" вызывающей функции. Аналогично переменные вызывающей функции не известны вызываемой функции. Вот почему для связи с ней, т. е. для передачи значений в нее и из нее, мы пользуемся аргументами и оператором return.
Переменные, известные только одной функции, а именно той, которая их содержит, называются "локальными" переменными. До сих пор это был единственный вид переменных, которыми мы пользовались, но в языке Си допускается наличие переменных, известных нескольким функциям. Такие нелокальные переменные называются "глобальными", и мы вернемся к ним позже. Теперь же мы хотим подчеркнуть, что локальные переменные являются действительно локальными. Даже в том случае, если мы используем одно и то же имя для переменных в двух различных функциях, компилятор (и, таким образом, компьютер "считает" их разными переменными. Мы можем показать это, используя операцию & (не путайте с операцией &&).
В результате выполнения операции & определяется адрес ячейки памяти, которая соответствует переменной. Если pooh - имя переменной, то &pooh - ее адрес. Можно представить себе адрес как ячейку памяти, но можно рассматривать его и как метку, которая используется компьютером, для идентификации переменной. Предположим, мы имеем оператор
pooh = 24;
Пусть также адрес ячейки, где размещается переменная pooh - 12126. Тогда в результате выполнения оператора
printf(" %d %d\n" , pooh, &pooh);
получим
24 12126
Более того, машинный код, соответствующий первому оператору, словами можно выразить приблизительно так: "Поместить число 24 в ячейку с адресом 12126".
Воспользуемся указанной выше операцией для проверки того, в каких ячейках хранятся значения переменных, принадлежащих разным функциям, но имеющих одно и то же имя.
/* контроль адресов */
main( )
{
int pooh = 2, bah = 5;
printf(" Вmain( ), pooh = %d и&pooh = %u \n" , pooh, &pooh);
printf('B main( ), bah = %d и&bah = %u\n>/, bah, &bah);
mikado(pooh);
}
mikado(bah) int bah;
{
int pooh = 10;
printf("B mikado( ), pooh = %d и&pooh = %u\n, pooh, &pooh);
printf(" Вmikado( ), bah = %d и&bah = %u\n" , bah, &bah);
}
Мы воспользовались форматом %u (целое без знака) для вывода на печать адресов на тот случай, если их величины превысят максимально возможное значение числа типа int. В нашей вычислительной системе результат работы этой маленькой программы выглядит так:
Вmain( ), pooh = 2 и&pooh = 56002
B main( ), bah = 5 и&bah = 56004
B mikado( ), pooh = 10 и&pooh =55996
В mikado( ), bah = 2 и &bah = 56000.
О чем это говорит? Во-первых, две переменные pooh имеют различные адреса. То же самое верно и относительно переменных bah. Следовательно, как и было обещано, компьютер рассматривает их как четыре разные переменные. Во-вторых, при вызове mikado(pooh) величина (2) фактического аргумента (pooh из main( )) передастся формальному аргументу (bah из mikado( )). Обратите внимание, что было передано только значение переменной. Адреса двух переменных (pooh в main( ) и bah в mikado( )) остаются различными.
Мы коснулись второго вопроса потому, что этот факт оказывается неверным для всех других языков. В той или иной процедуре Фортрана, например, можно использовать переменные вызывающей программы. Кроме того, в такой процедуре переменные могут иметь различные имена, но адреса их при этом будут совпадать. В языке Си подобные механизмы отсутствуют. Каждая функция использует свои собственные переменные. Это более предпочтительно, потому что "исходные" переменные не будут таинственным образом изменяться из-за того, что вызванная функция обладает побочным эффектом. Но это может также приводить и к некоторым трудностям, о чем и будет рассказано и следующем разделе.
Иногда требуется, чтобы одна функция могла изменять переменные, относящиеся к другой. Например, в задачах сортировки часто бывает необходимо осуществлять обмен значениями между двумя переменными. Предположим, у нас есть две переменные х и у и мы хотим, чтобы они обменялись своими значениями. Простая последовательность операторов
х = у;
y = х;
не является решением поставленной задачи, потому что к тому моменту, когда начнет выполняться оператор во второй строке, первоначальное значение переменной x будет потеряно. Чтобы сохранить это первоначальное значение, необходимо дополнить данный фрагмент еще одной строкой:
temp = х;
х = у;
у = temp;
Теперь у нас есть требуемый метод; реализуем его в виде некоторой функции, а также создадим драйвер для eе проверки. Чтобы сделать более ясным, какая переменная принадлежит функции main( ), а какая - функции interchange( ), мы будем использовать переменные х и у в первой из них, и u и v - во второй.
/* обмен1 */
main( )
{
int х = 5, у = 10;
printf(" Вначале х = %d и у = %d.\n" , х, у);
interchange(x, у);
prinlf(" Теперь х = %d и у = %d.\n" , х, у);
}
interchangce(u, v) int u, v;
{
int temp;
temp = u;
u = v;
v = temp;
}
Попробуем выполнить эту программу. Результаты будут выглядеть следующим образом:
Вначале х = 5 и у = 10.
Теперь х = 5 и у = 10.
Не может быть! Значения переменных не поменялись местами! Вставим в программу interchange( ) несколько операторов печати, чтобы понять причину допущенной ошибки.
/* обмен2 */
main( )
{
int х = 5, у = 10;
printf(" Вначале х = %d и у = %d.\n", х,у);
interchange(x, у);
printf(" Теперь х = %d и у = %d.\n", х, у);
}
interchange(u, v)
int u, v;
{
int temp;
printf(" Вначалеu = %d иv = %d.\n", u, v);
temp = u;u = v;v = temp;
printf(" Теперь u = %d и v = %d.\n", u, v);
}
Результат работы этой программы выглядит так:
Вначале x = 5 и y = 10.
Вначале u = 5 и v = 10.
Вначале u = 10 и v = 5.
Вначале x = 5 и y = 10.
Отсюда видно, что ничего неправильного в работе функции interchange( ) нет; она осуществляет обмен значениями между переменными u и v. Проблема состоит в передаче результатов обратно в функцию main( ). Как мы уже указывали, функции interchange( ) и main() используют различные переменные, поэтому обмен значениями между переменными u и v не оказывает никакого влияния на х и у! А нельзя ли каким-то образом воспользоваться оператором return? Мы могли бы, конечно, завершить тело функции interchange( ) строкой
return(u);
и изменить форму вызова в функции main( ) следующим образом:
х = interchange(x, у);
В результате такого обращения к функции переменная х получит новое значение, но у при этом не изменится.
С помощью оператора return в вызывающую программу можно передать только одну величину. Но нам нужно передать две величины. Это оказывается вполне осуществимым! Для этого нужно лишь воспользоваться "указателями".
Указатели? Что это такое? Вообще говоря, указатель - некоторое символическое представление адреса. Например, ранее мы воспользовались операцией получения адреса для нахождения адреса переменной pooh. В данном случае &pooh означает "указатель на переменную pooh". Фактический адрес - это число (в нашем случае 56002), а символическое представление адреса &pooh является константой типа указатель. После всего сказанного выше становится очевидным, что адрес ячейки, отводимой переменной pooh, в процессе выполнения программы не меняется.
В языке Си имеются и переменные типа указатель. Точно так же как значением переменной типа char является символ, а значением переменной типа int - целое число, значением переменной типа указатель служит адрес некоторой величины. Если мы дадим указателю имя ptr, то сможем написать, например, такой оператор
ptr = &pooh; /* присваивает адрес pooh переменной ptr */
Мы говорим в этом случае, что ptr "указывает на" pooh. Различие между двумя формами записи: ptr и &pooh, заключается в том, что ptr - это переменная, в то время как &pooh - константа. В случае необходимости мы можем сделать так, чтобы переменная ptr указывала на какой-нибудь другой объект:
ptr = &bah; /* ptr указывает на bah, а не на pooh */
Теперь значением переменной ptr является адрес переменной bah.
Предположим, мы знаем, что в переменной ptr содержится ссылка на переменную bah. Тогда для доступа к значению этой переменной можно воспользоваться операцией "косвенной адресации" (*). (Не путайте эту унарную операцию косвенной адресации с бинарной операцией умножения *).
val = *ptr; /* определение значения, на которое указывает ptr */
Последние два оператора, взятые вместе, эквивалентны следующему:
val = bah;
Использование операций получения адреса и косвенной адресации оказывается далеко не прямым путем к результату; отсюда и появление слова "косвенная" в названии операции.
I. Операция получения адреса &
Когда за этим знаком следует имя переменной, результатом операции является адрес указанной переменной.
Пример:
&nurse дает адрес переменной nurse.
II. Операция косвенной адресации
* Когда за этим таком следует указатель на переменную, результатом операции является величнна, помещенная и ячейку с указанным адресом.
Пример:
nurse = 22;pir = &nurse; /* указатель на nurse */ val = *ptr;
Результатом выполнения этого фрагмента является присваивание значения 22 переменной val.
Мы знаем, как описывать переменные типа int и других типов. Но как описать переменную типа "указатель"? На первый взгляд это можно сделать так:
pointer ptr; /* неправильный способ описания указателя */
Почему нельзя использовать такую запись? Потому что недостаточно сказать, что некоторая переменная является указателем. Кроме этого, необходимо сообщить еще, на переменную какого типа ссылается данный указатель! Причина заключается в том, что переменные разных типов занимают различное число ячеек, в то время как для некоторых операций, связанных с указателями, требуется знать объем отведенной памяти. Ниже приводятся примеры правильного описания указателей:
int *pi; /* указатель на переменную типа целого */
char *рс; /* указатель на символьную переменную */
float *pf, *pg; /* указатели на переменные с плавающей точкой */
Спецификация типа задает тип переменной, на которую ссылается указатель, а символ звездочка (*) определяет саму переменную как указатель. Описание вида int *pi; говорит, что pi - это указатель и что *pi - величина типа int.
РИС. 9.5. Описание и использование указателей.
Точно так же величина (*рс), на которую ссылается переменна рс, имеет тип char. Что можно сказать о самой переменной рс? Мы считаем, что она имеет тип "указатель на переменную типа char". Ее величина, являющаяся адресом,- это целое число без знака, поэтому при выводе на печать значения переменной рс мы будем пользоваться форматом %u.
Мы только прикоснулись к обширному и увлекательному миру указателей. Сейчас нашей целью является использование указателей для решения задачи об установлении связи между функциями. Ниже приводится программа, в которой указатели служат средством, обеспечивающим правильную работу функции, которая осуществляет обмен значениями переменных. Посмотрим, как она выглядит, выполним ее, а затем попытаемся понять, как она работает.
/* обмен3 */
main( )
{
int x = 5, у = 10;
printf(" Вначале x = %d и у = %d.\n" , x, у);
interchange(&x,&y); /* передача адресов функции */
printf(" Теперь x = %d и у = %d.\n", x, у);
}
interchange(u, v)
int *u, *v; /* u и v являются указателями */
{
int temp;
temp = *u; /* temp присваивается значение, на которое указывает u */
*u = *v;
*v = temp;
}
После всех встретившихся трудностей, проверим, работает ли этот вариант 1
Вначале x = 5 и y = 10.
Теперь x = 10 и y = 5.
Да программа работает. Посмотрим, как она работает. Во-первых, теперь вызов функции выглядит следующим образом:
interchange(&x, &y);
Вместо передачи значений х и у мы передаем их адреса. Это означает, что формальные аргументы u и v, имеющиеся в спецификации:
interchange(u,v)
при обращении будут заменены адресами и, следовательно, они должны быть описаны как указатели. Поскольку х и у - целого типа, u и v являются указателями на переменные целого типа, и мы вводим следующее описание:
int *u, *v;
Далее в теле функции оператор описания:
int temp;
используется с целью резервирования памяти. Мы хотим поместить значение переменной х в переменную temp, поэтому пишем:
temp = *u;
Вспомните, что значение переменной u - это &х, поэтому переменная u ссылается на х. Это означает, что операция *u дает значение x, которое как раз нам и требуется. Мы не должны писать, например, так:
temp = u; /* неправильно */
поскольку при этом происходит запоминание адреса переменной х, а не ее значения; мы же пытаемся осуществить обмен значениями, а не адресами.
Точно так же, желая присвоить переменной у значение переменной х, мы пользуемся оператором:
*u = *v;
который соответствует оператору
x = y;
Подведем итоги. Нам требовалась функция, которая могла бы изменять значения переменных х и у. Путем передачи функции адресов переменных х и у мы предоставили ей возможность доступа к ним. Используя указатели и операцию *, функция смогла извлечь величины, помещенные в соответствующие ячейки памяти, и поменять их местами.
Вообще говоря, при вызове функции информация о переменной может передаваться функции в двух видах. Если мы используем форму обращения:
function1(х);
происходит передача значения переменной х. Если же мы используем форму обращения:
function2(&x);
происходит передача адреса переменной х. Первая форма обращения требует, чтобы определение функции включало в себя формальный аргумент того же типа, что и х:
functionl(num)
int num;
Вторая форма обращения требует, чтобы определение функции включало в себя формальный аргумент, являющийся указателем на объект соответствующего типа:
function2(ptr)
int *ptr;
Пользуйтесь первой формой, если входное значение необходимо функции для некоторых вычислений или действий, и второй формой, если функция должна будет изменять значения переменных в вызывающей программе. Вторая форма вызова уже применялась при обращении к функции scanf( ). Когда мы хотим ввести некоторое значение в переменную num, мы пишем scanf("%d, &num). Данная функция читает величину, затем, используя адрес, который ей дается, помещает эту величину в память.
Указатели позволяют обойти тот факт, что переменные функции interchange( ) являются локальными. Они дают возможность нашей функции "добраться" до функции main( ) и изменить величины описанных в ней объектов.
Программисты, работающие на языке Паскаль, могут заметить, что первая форма вызова аналогична обращению с параметром-значением, а вторая - с параметром-переменной. У программистов, пишущих на языке Бейсик, понимание всей этой методики может вызвать некоторые затруднения. В этом случае если материал данного раздела покажется вам поначалу весьма не обычным, не сомневайтесь, что благодаря некоторой практике, все обсуждаемые средства станут простыми, естественными и удобными.
Наше обсуждение указателей строится на рассмотрении связей между именами, aдреcaми и значениями переменных; дальше мы продолжим обсуждение этих вопросов.
При написании программы мы представляем себе переменную как объект, имеющий два атрибута: имя и значение. (Кроме указанных, существуют еще и другие атрибуты, например тип, но это уже другой вопрос). После компиляции программы и загрузки в память "с точки зрения машины" данная переменная имеет тоже два атрибута: адрес и значение. Адрес - это машинный вариант имени.
Во многих языках программирования адрес объекта скрыт от программиста и считается относящимся к уровню машины. В языке Си благодаря операции & мы имеем возможность узнать и использовать адрес переменной:
&bаrn - это адрес переменной bаrn.
Мы можем получить значение переменной, соответствующее данному имени, используя только само имя:
printf(" %d\n", barn) печатает значение переменной barn
Мы можем также получить значение переменной, исходя из ее адреса, при помощи операции *:
РИС. 9.6. Имена, адреса и величнны в системе с "байтовой адресацией" тина IBM PC.
Дано pbarn = &bаrn; тогда *pbarn - это величина, помещенная по адресу &bаrn. Хотя мы и можем напечатать адрес переменной для удовлетворения своего любопытства, это не основное применение операции &. Более важным является то, что наличие операций &, * и указателей позволяет обрабатывать адреса и их содержимое в символическом виде, чем мы и занимались в программе обмен3.
Теперь, когда мы знаем о функциях немного больше, соберем вместе несколько поучительных примеров, но сначала решим, чем мы будет заниматься.
Что вы скажете насчет функции возведения в степень, которая дает возможность возводить 2 в 5-ю степень или 3 в 3-ю и т. д.? Во-первых, необходимо решить, что будет служить входом программы. Это понятно: Cи требуется знать число, возводимое в степень, и показатель степени. Достичь этого можно путем введения двух аргументов:
powеr(base, exp)
int base, exp;
(Мы ограничились здесь целыми числами, а также тем предположением, что результат будет сравнительно невелик.)
Далее требуется решить, что будет выходом функции. Ответ, конечно, тоже очевиден. Выходом должно быть одно число, являющееся значением переменной answer. Мы можем реализовать это с помощью оператора
rеturn(answcr);
Теперь для получения требуемого результата выберем алгоритм:
установим переменную answer равной 1,
умножим answer на base столько раз, сколько указывает exp.
Возможно, не совсем ясно, как осуществить второй шаг, поэтому разобьем его дальше на более мелкие шаги:
умножим answer на base и уменьшим на 1, остановимся, когда exp станет равной 0.
Если значение exp равно, скажем 3, тогда использование такого алгоритма приведет к трем умножениям; поэтому данный способ кажется вполне разумным.
Очень хорошо. Выразим теперь этот алгоритм в виде программы на языке Си.
/* возводит основание в степень */
power(base, exp)
int base, exp;
{
int answer;
for (answer = 1; exp > 0; exp--)
answer = answer* base;
return(answer);
}
Теперь проверим ее работу с помощью драйвера.
/* проверка возведения в степень */
main( )
{
int x;
х= power(2,3);
printf(" %d\n", x);
x = power(-3,3);
prinif(" %d\n", x);
x = power(4, -2);
printf(" %d\n", x);
x = power(5, 10);
printf(" %d\n", x);
}
Объединим указанные две функции, проведем компиляцию и выполним данную программу. Результаты оказываются следующими:
8
-27
1
761
Итак, 2 в 3-й степени - это 8, а - 3 в 3-й равно -27. Пока все правильно. Но 4 в степени -2 равно 1/16, а не 1. А 5 в 10-й степени, если память нам не изменяет,- это 9 765 625. В чем дело? Во-первых, программа не предназначалась для обработки отрицательных степеней, поэтому она и не смогла справиться с этой задачей. Во-вторых, в нашей системе величины типа int не могут превосходить 65 535.
Можно расширить программу путем включения в нее обработки отрицательных степеней и использования чисел с плавающей точкой для представления переменных base и answer. В любом случае показатель степени должен выражаться целым числом, потому что это число выполняемых умножений; нельзя произвести 2,31 умножения.
/* возводит основание в степень*/
double powеr(base, еxp)
double, base;
int exp;
{
double answer;
if(exp > 0)
{
for(answer = 1.0; exp > 0; exp --) answer * = base;
return(answer);
}
else if(base != 0)
{
for(answer = 1.0; exp < 0; exp++ ) answer /= base;
return(answer);
}
else /* base = 0 иеxp <= 0 */
{
printf(" Нельзя возводить 0 в %d стeпень!\n", exp);
return(0);
}
}
Необходимо отметить здесь несколько моментов.
Первый: самым главным является то, что мы должны описать тип функции! Переменная answer имеет тип double, следовательно, сама функция power() тоже должна быть типа double, так как ей присваивается величина, возвращаемая оператором return. Почему, спросите вы, мы не описывали тип функции раньше? Дело в том, что по умолчанию в языке Си функция имеет тип int (для большинства функций это так), если не указано что-то иное.
Второй: мы хотели показать, что не забыли те новые операции присваивания, которые ввели в гл. 8.
Третий: в соответствии с алгебраическими правилами возведение в отрицательную степень было преобразовано в деление. Это внесло опасность деления на нуль, но в данном случае мы предусмотрели выдачу сообщения об ошибке и возврат значения 0, чтобы работа программы не прекращалась.
Мы можем воспользоваться тем же драйвером при условии, что тип функции power( ) там тоже описан.
/* проверка возведения в степень*/
main( )
{
double x;
double power( ); /* это пример oписания функции */
x = power(2.0, 3);
printf(" %.0f \n", x);x = power(-3.0, 3);
printf(" %.0f\n" , x);x = power(4.0, -2);
printf(" %.4f\n", x);x = power(5.0, 10);
print f ("%.0f \n", x);
}
На этот раз результаты работы программы выглядят вполне удовлетворительно.
8
-27
0.0625
9765625
Данный пример побуждает нас ввести следующий короткий раздел.
Тип функции определяется типом возвращаемого ею значения, а не типом ее аргументов. Если указание типа отсутствует, то по умолчанию считается, что функция имеет тип int. Если значения функции не принадлежат типу int, то необходимо указать ее тип в двух местах.
1. Описать тип функции в ее определении:
char pun(ch, n) /* функция возвращает символ */
int n;
char ch;
float raft(num) /* функция возвращает величину типа float */
int num;
2. Описать тип функции также в вызывающей программе. Описание функции должно быть приведено наряду с описаниями переменных программы; необходимо только указать скобки (но не аргументы) для идентификации данного объекта как функции.
main( )
{
char rch, pun( );
float raft;
}
Запомните! Если функция возвращает величину не типа int, указывайте тип функции там, где она определяется, и там, где она используется.
1. Форма записи
Типичное определение функции имеет следующий вид:
имя (список аргументов)
описание аргументов
тело функции
Наличие списка аргументов и описаний не является обязательным. Переменные, отличные от аргументов, описываются внутри тела, которое заключается в фигурные скобки.
Пример:
diff(x, у) /* имя функции и список аргументов */
ini x, у; /* описание аргументов */
{ /* начало тела функции */
int z; /* описание локальной переменной */
z = х - у;return(z);
} /* конец тела функции */
II. Передача значений функции:
Аргументы используются для передачи значений из вызывающей программы и функцию. Если значения переменных а и b будут 5 и 2, то при вызове
с = diff(а,b);
осуществляется передача этих значений переменным х и у. Значения 5 и 2 называют ся фактическими аргументами, а переменные х и у, указанные в описании функции: diff( ) - формальными аргументами.
Использование ключевого слова return позволяет передавать в вызывающую программу одно значение из вызываемой функции. В нашем примере переменной с присваивается значение переменной z, равное 3.
Обычно выполнение функции не оказывает никакого влияния на значения переменных вызывающей программы. Чтобы иметь возможность непосредственно изменять значения переменных вызывающей программы, необходимо использовать указатели в качестве аргументов. Это может оказаться необходимым в случае, если в вызывающую программу требуется передать более чем одно значение.
III. Тип функции
Функции должны иметь тот же тип, что и значения, которые они возвращают в качестве результатов. По умолчанию предполагается, что функции имеют тип int. Если функция имеет другой тип, он должен быть указан и в вызывающей программе, и в самом определении функции.
Пример
main( )
{
float q, x, duff( ); /* описание в вызывающей программе */
int n;
...
q = duff(х, n);
...
}
float duff(u, k); /* описание в определении функции */
float u;
int k;
{
float tor;
...
return(tor); /* возвращает значение типа float */
}
Все функции в программе, написанной на языке Си, равноправны: каждая из них может вызывать любую другую функцию и в свою очередь каждая может быть вызвана любой другой функцией. Это делает функции языка Си несколько отличными от процедур Паскаля, поскольку процедуры в Паскале могут быть вложены в другие процедуры (причем, процедуры, содержащиеся в одном гнезде, являются недоступными для процедур, расположенных в другом).
Нет ли у функции main( ) какой-то специфики? Безусловно, есть; она заключается в том, что после "сборки" программы, состоящей из нескольких функций, ее выполнение начинается с первого оператора функции main( ). Но этим ее исключительность и граничивается. Даже функция main( ) может быть вызвана другими функциями, как показывает приведенный ниже пример:
/* вызовфункции main( ) */
#include
main( )
{
char ch;
printf (" Укажите произвольный символ. Q - признак конца работы. \n");
ch = getchar( );
printf ("Так! Вы указали %с!\n", ch);
if(ch != 'Q') more( );
}
more( );
{
main( );
}
Функция main( ) вызывает more(), а функция more() вызывает main()! После вызова функции main( ) ее выполнение начинается с самого начала; мы организовали цикл с взаимным вызовом.
Функция может даже вызывать сама себя. Упростим предыдущий пример следующим образом:
/* main.main */
#include
main( )
{
char ch;
printf (" Укажите произвольный символ. Q - признак конца работы.\n");
ch = getchar( );
printf ("Так! Вы указали %с!\n", ch);
if(ch != 'Q') main( );
}
Ниже приводятся результаты одного прогона программы, показывающие, что она работает. Обратите внимание на то, как обрабатывается символ "новая строка", который передается программе при нажатии клавиши [ввод].
Введите произвольный символ. Q - признак конца работы.
I
Так! Вы указали I !
Введите произвольный символ. Q - признак конца работы.
!
Так! Вы указали ! !
Введите произвольный символ. Q - признак конца работы.
Q
Так! Вы указали Q !
Действие, состоящее в том, что функция вызывает сама себя, называется "рекурсией". Цикл, который мы создали, используя рекурсию, отличается от циклов while и do while. Когда функция main( ) вызывает сама себя, не происходит передачи управления на ее начало. Вместо этого в памяти машины создаются копни всего набора переменных функции main( ). Если вы выведете на печать адреса переменных в обычном цикле, то увидите, что эти адреса не изменяются от итерации к итерации. Что же касается рассматриваемого здесь цикла, то в нем адрес используемой переменной меняется, поскольку при каждом выполнении тела цикла создается новая копия переменной ch. Если программа циклически выполняется 20 раз, то будет создано 20 различных копий переменной, каждая из которых носит имя ch, но имеет свой собственный адрес.
Простейший способ использования нескольких функций в одной программе заключается в том, чтобы поместить их в один файл, после чего осуществить компиляцию программы, содержащейся в этом файле так, как будто она состояла из одной функции.
Второй способ заключается и применении директивы #include. Если одна функция содержится в файле с именем file1.с, а вторая и файле file2.c, поместите эту директиву в файл filel.c:
#include "file2.c"
Дополнительная информация о директиве #include находится в гл. 11. Другие возможные способы являются в большей степени системнозависимыми. Вот некоторые из них:
Предположим, file1.с и file2.c - два файла, содержащие программные тексты, соответствующие функциям языка Си. В результате выполнения команды
cc file1.c file2.c
будет осуществлена компиляция функций, содержащихся в обоих файлах, и получен файл выполняемого кода с именем a.out. Кроме того, будут созданы два файла с "объектным" кодом - file1.0 и file2.0. Если позже вы измените текст, содержащийся в файле с именем filel.с, а второй файл оставите без изменений, то сможете осуществить компиляцию первого файла, а затем объединить полученный объектный код с объектным кодом, соответствующим второму файлу, при помощи команды
cc file1.c file2.0
Выполните раздельную компиляцию функции, содержащихся в файлах filel.c и file2.c; в результате будут получены два файла с объектным кодом - file1.obj и file2.obj. Используйте системный редактор связей для объединения их друг с другом и со стандартным объектным модулем с.obj:
link с filel file2
Некоторые из таких систем позволяют компилировать функции, содержащиеся в нескольких файлах, сразу так же, как в ОС UNIX с помощью команды:
сс filel.с file2.c
или какого-то ее эквивалента. В некоторых случаях вы можете получить отдельные модули с кодом ассемблера, а затем объединить их, используя процесс ассемблирования.
Для создания больших программ вы должны использовать функции в качестве "строительных блоков". Каждая функция должна выполнять одну вполне определенную задачу. Используйте аргументы для передачи значений функции и ключевое слово return для передачи результирующего значения в вызывающую программу. Если возвращаемое функцией значение не принадлежит типу int, вы должны указать тип функции в ее определении и в разделе описаний вызывающей программы. Если вы хотите, чтобы при выполнении функции происходило изменение значении переменных в вызывающей программе, вы должны пользоваться адресами и указателями.
Как определять функцию.
Как передавать функции информацию: при помощи аргументов.
Различие между формальным и фактическим аргументами: первый является переменной, используемой функцией, а второй - значением, поступающим из вызывающей функции.
Где необходимо описывать аргументы: после имени функции и перед первой фигурной скобкой.
Где необходимо описывать остальные локальные переменные: после первой фигурной скобки.
Когда и как использовать оператор return.
Когда и как использовать адреса и указатели для доступа к объектам.
1. Напишите функцию, возвращающую сумму двух целых чисел.
2. Какие изменения должны были бы произойти с функцией из вопроса 1, если вместо целых складывались бы два числа типа float?
3. Напишите функцию alter( ), которая берет две переменные х и у типа int и заменяет соответственно на их сумму и разность.
4. Проверьте, все ли правильно в определении функции, приведенной ниже?
salami(num)
{
int num, count;
for(count = 1; count <= num; num++) printf(" Осалями!\n");
}
1.
sum(j,k) int j, k;
{ return(j+k); }
2.
float sum(j,k) float j,k;
Необходимо также привести описание функции float sum( ) и вызывающей программе.
3. Поскольку мы хотим изменить две переменные в вызывающей программе, можно воспользоваться адресами и указателями. Обращение к функции будет выглядеть так: alter(&x,&y). Возможное решение имеет следующий вид:
alter(px, ру)
int *рх, *ру; /* указатели на х и у*/
{
int sum, diff;
sum = *рх + *ру; /* складывает содержимое двух переменных, определяемых адресами */
diff = *рх- *ру;
*рх= sum;
*ру = diff;
}
4. Нет; переменная num должна быть описана перед первой фигурной скобкой, а не после нее. Кроме того, выражение num++ необходимо заменить на count++.
1. Напишите функцию mах(х, у), возвращающую большее из двух значении.
2. Напишите функцию chllne(ch, i, j), печатающую запрошенный символ с i-й пo j-ю позиции. Смотри программу художник-график, приведенную в гл. 7.