Урок 8. Функции. Передача аргументов по значению и по ссылке

Общее представление

Язык C как и большинство других языков программирования позволяет создавать программы, состоящие из множества функций, а также из одного или нескольких файлов исходного кода. До сих пор мы видели только функцию main(), которая является главной в программе на C, поскольку выполнение кода всегда начинается с нее. Однако ничего не мешает создавать другие функции, которые могут быть вызваны из main() или любой другой функции. На этом уроке мы рассмотрим создание только однофайловых программ, содержащих более чем одну функцию.

При изучении работы функций важно понимать, что такое локальная и что такое глобальная переменные. В языке программирования C глобальные (внешние) переменные объявляются вне какой-либо функции. С их помощью удобно организовывать обмен данными между функциями, однако это считается дурным тоном, т.к. легко запутывает программу. Локальные переменные в языке программирования C называют автоматическими. Область действия автоматических переменных распространяется только на ту функцию, в которой они были объявлены. Параметры функции также являются локальными переменными.

Структурная организация файла программы на языке C, содержащего несколько функций, может выглядеть немного по-разному. Т.к. выполнение начинается с main(), то ей должны быть известны спецификации (имена, количество и тип параметров, тип возвращаемого значения) всех функций, которые из нее вызываются. Отсюда следует, что объявляться функции должны до того, как будут вызваны. А вот определение функции уже может следовать и до и после main(). Рассмотрим такую программу:

#include <stdio.h>
 
float median (int a, int b); // объявление функции
 
main () {
  int num1 = 18, num2 = 35;
  float result;
 
  printf("%10.1f\n", median(num1, num2));
  result = median(121, 346);
  printf("%10.1f\n", result);
  printf("%10.1f\n", median(1032, 1896));
}
 
float median (int n1, int n2) { // определение функции
  float m;
 
  m = (float) (n1 + n2) / 2;
  return m;
}

В данном случае в начале программы объявляется функция median(). Объявляются тип возвращаемого ею значения (float), количество и типы параметров (int a, int b). Обратите внимание, когда объявляются переменные, то их можно группировать: int a, b;. Однако с параметрами функций так делать нельзя, для каждого параметра тип указывается отдельно: (inta, int b).

Далее идет функция main(), а после нее — определение median(). Имена переменных-параметров в объявлении функции никакой роли не играют (их вообще можно опустить, например, float median (int, int);). Поэтому когда функция определяется, то имена параметров могут быть другими, однако тип и количество должны строго совпадать с объявлением.

Функция median() возвращает число типа float. Оператор return возвращает результат выполнения переданного ему выражения; после return функция завершает свое выполнение, даже если далее тело функции имеет продолжение. Функция median() вычисляет среднее значение от двух целых чисел. В выражении (float) (n1 + n2) / 2 сначала вычисляется сумма двух целых чисел, результат преобразуется в вещественное число и только после этого делится на 2. Иначе мы бы делили целое на целое и получили целое (в таком случае дробная часть просто усекается).

В теле main() функция median() вызывается три раза. Результат выполнения функции не обязательно должен быть присвоен переменной.

Вышеописанную программу можно было бы записать так:

#include <stdio.h>
 
float median (int n1, int n2) {
  float m;
 
  m = (float) (n1 + n2) / 2;
  return m;
}
 
main () {
  int num1 = 18, num2 = 35;
  float result;
 
  printf("%10.1f\n", median(num1, num2));
  result = median(121, 346);
  printf("%10.1f\n", result);
  printf("%10.1f\n", median(1032, 1896));
}

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

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

Статические переменные

В языке программирования C существуют так называемые статические переменные. Они могут быть как глобальными, так и локальными. Перед именем статической переменной пишется ключевое слово static.

Внешние статические переменные, в отличие от обычных глобальных переменных, нельзя использовать из других файлов в случае программы, состоящей не из одного файла. Они глобальны только для функций того файла, в котором объявлены. Это своего рода сокрытие данных, по принципу "не выставлять наружу ничего лишнего, чтобы 'что-нибудь' нечаянно не могло 'испортить' данные".

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

#include <stdio.h>
 
int hello();
 
main() {
  printf(" - %d-й вызов\n", hello());
  printf(" - %d-й вызов\n", hello());
  printf(" - %d-й вызов\n", hello());
}
 
int hello () {
  static count = 1;
  printf("Hello world!");
  return count++;
}

Результат:

Hello world! - 1-й вызов 
Hello world! - 2-й вызов 
Hello world! - 3-й вызов

В этом примере в функции hello() производится подсчет ее вызовов.

Задание
Придумайте свой пример с использованием статической переменной.

Передача аргументов по ссылке

В первом примере этого урока мы передавали в функцию аргументы по значению. Это значит, что когда функция вызывается, ей передаются в качестве фактических параметров (аргументов) не указанные переменные, а копии значений этих переменных. Сами переменные к этим копиям уже никакого отношения не имеют. В вызываемой функции эти значения присваиваются переменным-параметрам, которые, как известно, локальны. Отсюда следует, что изменение переданных значений никакого влияния на переменные, переданные в функцию при вызове, не оказывают. В примере выше даже если бы в функции median() менялись значения переменных n1 и n2, то никакого влияния сей факт на переменные num1 и num2 не оказал.

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

#include <stdio.h>
 
void multi (int *px, int y);
 
main () {
  int x = 34, y = 6;
 
  multi(&x, 367);
  multi(&y, 91);
  printf("%d %d\n", x, y);
}
 
void multi (int *base, int pow) {
  while (pow >= 10) {
    *base = *base * 10;
    pow = pow / 10;
  }
}

Функция multi() ничего не возвращает, что подчеркнуто с помощью ключевого слова void. Принимает эта функция адрес, который присваивается локальной переменной-указателю, и целое число. В теле функции происходит изменение значения по адресу, содержащемуся в указателе. Но по сути это адрес переменной x из фукнции main(), а значит меняется и ее значение.

Когда multi() вызывается в main(), то в качестве первого параметра мы должны передать адрес, а не значение. Поэтому, например, вызов multi(x, 786) привел бы к ошибке, а вызов multi(&x, 786) — правильный, т.к. мы берем адрес переменной x и передаем его в функцию. При этом ничего не мешает объявить в main() указатель и передавать именно его (в данном случае сама переменная p содержит адрес):

  int x = 34, y = 6;
  int *p;
 
  p = &x;
  multi(p, 367);
  p = &y;
  multi(p, 367);
  printf("%d %d\n", x, y);

Кроме того, следует знать, что функция может возвращать адрес.

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

Решение задач

Задания

  1. Перепишите код первого примера этого урока так, чтобы в нем использовался указатель; а код примера с функцией multi(), наоборот, избавьте от указателей.
  2. Напишите программу, в которой помимо функции main() были бы еще две функции: в одной вычислялся факториал переданного числа, в другой — находился n-ый элемент ряда Фибоначчи (n — параметр функции). Вызовите эти функции с разными аргументами.
  3. Придумайте и напишите программу, в которой из функции main() вызывается другая функция, а из последней вызывается еще одна функция.

Создано