Содержание | <<< | >>>

Аргументы функции

Если функция должна принимать аргументы, то в ее объявлении следует декларировать параметры, которые примут значения этих аргументов. Как видно из объявления следующей функции, объявления параметров стоят после имени функции.

/* Возвращает 1, если символ c входит в строку s;
   и 0 в противном случае. */
int is_in(char *s,  char c)
{
  while(*s)
    if(*s==c) return 1;
    else s++;
  return 0;
}

Функция is_in() имеет два параметра: s и d. Если символ c входит в строку s, то эта функция возвращает 1, в противном случае она возвращает 0.

Хотя параметры выполняют специальную задачу, — принимают значения аргументов, передаваемых функции, — они все равно ведут себя так, как и другие локальные переменные. Формальным параметрам функции, например, можно присваивать какие-либо значения или использовать эти параметры в каких-либо выражениях.

Вызовы по значению и по ссылке

В языках программирования имеется два способа передачи значений подпрограмме. Первый из них — вызов по значению. При его применении в формальный параметр подпрограммы копируется значение аргумента. В таком случае изменения параметра на аргумент не влияют.

Вторым способом передачи аргументов подпрограмме является вызов по ссылке. При его применении в параметр копируется адрес аргумента. Это значит, что, в отличие от вызова по значению, изменения значения параметра приводят к точно таким же изменениям значения аргумента.

За небольшим количеством исключений, в языке С для передачи аргументов используется вызов по значению. Обычно это означает, что код, находящийся внутри функции, не может изменять значений аргументов, которые использовались при вызове функции.

Проанализируйте следующую программу:

#include <stdio.h>

int sqr(int x);

int main(void)
{
  int t=10;

  printf("%d %d", sqr(t), t);

  return 0;
}

int sqr(int x)
{
  x = x*x;
  return(x);
}

В этом примере в параметр х копируется 10 — значение аргумента для sqr(). Когда выполняется присваивание х=х*х, модифицируется только локальная переменная х. А значение переменной t, использованной в качестве аргумента при вызове sqr(), по-прежнему остается равным 10. Поэтому выведено будет следующее: 100.10.

Помните, что именно копия значения аргумента передается в функцию. А то, что происходит внутри функции, не влияет на значение переменной, которая была использована при вызове в качестве аргумента.

Вызов по ссылке

Хотя в С для передачи параметров применяется вызов по значению, можно создать вызов и по ссылке, передавая не сам аргумент, а указатель на него[1]. Так как функции передается адрес аргумента, то ее внутренний код в состоянии изменить значение этого аргумента, находящегося, между прочим, за пределами самой функции.

Указатель передается функции так, как и любой другой аргумент. Конечно, в таком случае параметр следует декларировать как один из типов указателей. Это можно увидеть на примере функции swap(), которая меняет местами значения двух целых переменных, на которые указывают аргументы этой функции:

void swap(int *x, int *y)
{
  int temp;

  temp = *x;  /* сохранить значение по адресу x */
  *x = *y;    /* поместить y в x */
  *y = temp;  /* поместить x в y */
}

Функция swap() может выполнять обмен значениями двух переменных, на которые указывают х и y, потому что передаются их адреса, а не значения. Внутри функции, используя стандартные операции с указателями, можно получить доступ к содержимому переменных и провести обмен их значений[2].

Помните, что swap() (или любую другую функцию, в которой используются параметры в виде указателей) необходимо вызывать вместе с адресами аргументов[3]. Следующая программа показывает, как надо правильно вызывать swap():

#include <stdio.h>
void swap(int *x, int *y);

int main(void)
{
  int i, j;

  i = 10;
  j = 20;

  printf("i и j перед обменом значениями: %d %d\n", i, j);

  swap(&i, &j); /* передать адреса переменных i и j */

  printf("i и j после обмена значениями: %d %d\n", i, j);

  return 0;
}

void swap(int *x, int *y)
{
  int temp;

  temp = *x;  /* сохранить значение по адресу x */
  *x = *y;    /* поместить y в x */
  *y = temp;  /* поместить x в y */
}

И вот что вывела эта программа:

i и j перед обменом значениями: 10 20
i и j после обмена значениями: 20 10

В программе переменной i присваивается значение 10, а переменной j — значение 20. Затем вызывается функция swap() с адресами этих переменных. (Для получения адреса каждой из переменных используется унарный оператор &.) Поэтому в swap() передаются адреса переменных i и j, а не их значения.

На заметкуЯзык C++ при помощи параметров-ссылок дает возможность полностью автоматизировать вызов по ссылке. А в языке С параметры-ссылки не поддерживается

Вызов функций с помощью массивов

Подробно о массивах рассказывалось в главе 4. В настоящем же разделе рассказывается о передаче массивов функциям в качестве аргументов. Этот вопрос рассматривается потому, что эта операция является исключением по отношению к обычной передаче параметров, выполняемой путем вызова по значению[4].

Когда в качестве аргумента функции используется массив, то функции передается его адрес. В этом и состоит исключение по отношению к правилу, которое гласит, что при передаче параметров используется вызов по значению. В случае передачи массива функции ее внутренний код работает с реальным содержимым этого массива и вполне может изменить это содержимое. Проанализируйте, например, функцию print_upper(), которая печатает свой строковый аргумент на верхнем регистре:

#include <stdio.h>
#include <ctype.h>

void print_upper(char *string);

int main(void)
{
  char s[80];

  printf("Введите строку символов: ");
  gets(s);
  print_upper(s); 
  printf("\ns теперь на верхнем регистре: %s", s);
  return 0;
}

/* Печатать строку на верхнем регистре. */
void print_upper(char *string)
{
  register int t;

  for(t=0; string[t]; ++t)  {
    string[t] = toupper(string[t]);
    putchar(string[t]);
  }
}

Вот что будет выведено в случае фразы "This is a test." (это тест):

Введите строку символов: This is a test.
THIS IS A TEST.
s теперь в верхнем регистре: THIS IS A TEST.

Правда, эта программа не работает с символами кириллицы.

После вызова print_upper() содержимое массива s в main() переводится в символы верхнего регистра. Если вам это не нужно, программу можно написать следующим образом:

#include <stdio.h>
#include <ctype.h>

void print_upper(char *string);

int main(void)
{
  char s[80];

  printf("Введите строку символов: ");
  gets(s);
  print_upper(s);
  printf("\ns не изменялась: %s", s);

  return 0;
}

void print_upper(char *string)
{
  register int t;

  for(t=0; string[t]; ++t)
    putchar(toupper(string[t]));
}

Вот какой на этот раз получится фраза "This is a test.":

Введите строку символов: This is a test.
THIS IS A TEST.
s не изменилась: This is a test.

На этот раз содержимое массива не изменилось, потому что внутри print_upper() не изменялись его значения.

Классическим примером передачи массивов в функции является стандартная библиотечная функция gets(). Хотя gets(), которая находится в вашей стандартной библиотеке, и более сложная, чем предлагаемая вам версия xgets(), но с помощью функции xgets() вы сможете получить представление о том, как работает gets().

/* Упрощенная версия стандартной библиотечной функции gets(). */
char *xgets(char *s)
{
  char ch, *p;
  int t;

  p = s;  /* xgets() возвращает указатель s */

  for(t=0; t<80; ++t){
    ch = getchar();

    switch(ch) {
      case '\n':
        s[t] = '\0'; /* завершает строку */
        return p;
      case '\b':
        if(t>0) t--;
        break;
      default:
        s[t] = ch;
    }
  }
  s[79] = '\0';
  return p;
}

Функцию xgets() следует вызывать с указателем char *. Им, конечно же, может быть имя символьного массива, которое по определению является указателем char *. В самом начале программы xgets() выполняется цикл for от 0 до 80. Это не даст вводить с клавиатуры строки, содержащие более 80 символов. При попытке ввода большего количества символов происходит возврат из функции. (В настоящей функции gets() такого ограничения нет.) Так как в языке С нет встроенной проверки границ, программист должен сам позаботиться, чтобы в любом массиве, используемом при вызове xgets(), помещалось не менее 80 символов. Когда символы вводятся с клавиатуры, они сразу записываются в строку. Если пользователь нажимает клавишу <Backspase>, то счетчик t уменьшается на 1, а из массива удаляется последний символ, введенный перед нажатием этой клавиши. Когда пользователь нажмет <ENTER>, в конец строки запишется нуль, т.е. признак конца строки. Так как массив, использованный для вызова xgets(), модифицируется, то при возврате из функции в нем будут находиться введенные пользователем символы.

----------

[1]Конечно, при передаче указателя будет применен вызов по значению, и сам указатель внутри функции вы изменить не сможете. Однако для того объекта, на который указывает этот указатель, все произойдет так, будто этот объект был передан по ссылке. В некоторых языках программирования (например, в Алголе-60) имелись специальные средства, позволяющие уточнить, как следует передавать аргументы: по ссылке или по значению. Благодаря наличию указателей в С механизм передачи параметров удалось унифицировать. Параметры, не являющиеся массивами, в С всегда вызываются только по значению, но все, что в других языках вы можете сделать с объектом, получив ссылку на него (т.е. его адрес), вы можете сделать, получив значение указателя на этот объект (т.е. опять же, его адрес). Так что в языке С благодаря свойственной ему унификации передачи параметров никаких проблем не возникает. А вот в других языках трудности, связанные с отсутствием эффективных средств работы с указателями, встречаются довольно часто.

[2]Конечно, задача, решаемая этой программой, кажется тривиальной. Ну разве представляет трудность написать на каком-либо процедурном языке, например, на Алголе-60, процедуру, которая обменивает значения своих параметров. Ведь так просто написать: procedure swap(x, y); integer х, y; begin integer t; t:= x; x:=y; y:=t end. Но эта процедура работает неправильно, хотя вызов значений здесь происходит по ссылке! Причем сразу найти тестовый пример, демонстрирующий ошибочность этой процедуры, удается далеко не всем. Ведь в случае вызова swap(i, j) все работает правильно! А что будет в случае вызова swap(i, a[i])? Да и можно ли на Алголе-60 вообще написать требуемую процедуру? Если вы склоняетесь к отрицательному ответу, то это показывает, насколько все-таки необходимы указатели в развитых языках программирования. Если все же вы знаете правильный ответ, то обратите внимание на то, что требуемая процедура, хотя и не длинная, но все же содержит своего рода программистский фокус!

[3]Конечно, это просто программистский жаргон. На самом деле, конечно, аргументами являются именно адреса переменных, а не сами переменные. Просто в этом случае для краткости изложения программисты "делают вид", что вроде бы и в самом деле происходит передача значений по ссылке.

[4]Ведь при вызове по значению пришлось бы копировать весь массив!


Содержание | <<< | >>>