Функции в C++

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

Например, можно создать подпрограмму, которая будет печатать все элементы массива а на экране. Теперь, чтобы вывести массив, хватит одной команды (вызова подпрограммы). При этом массив а должен быть доступен в тех местах программы, где мы обращаемся к подпрограмме (например, массив может быть глобальным).

Чтобы подпрограмма могла выполнять не только одинаковые, но и однотипные действия (похожие алгоритмы, но с различающимися параметрами), был придуман механизм передачи параметров из основной программы в подпрограмму. В подпрограмме используются некоторые абстрактные переменные (называемые аргументами или параметрами), конкретные значения которых задаются только в момент вызова подпрограммы.

Например, с использованием параметров можно создать подпрограмму, печатающую элементы не только массива а, но и любого другого, размер и имя которого мы передадим подпрограмме при её вызове через параметры.

Вызовом подпрограммы называется обращение к ней по имени с целью её использования. Вызов — это запуск подпрограммы.

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

Итак, конкретизируем понятие функции:

Функция — это определенная группа операций с уникальным именем, которая может:

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

Также функцию можно рассматривать как операцию, созданную самим разработчиком.

Известный примером функции является main. Она автоматические вызывается при запуске программы.

Создание функции

До того, как функция будет вызвана, она должна быть объявлена.

Объявление функции, аналогично объявлению переменной, указываются имя функции, тип значения, которое она может возвращать, набор её параметров (для каждого параметра задаётся тип и, при желании, имя).

Объявление функции называют также её прототипом.

Схема:

Тип_результата Имя_функции (Тип_пар1, Тип_пар2, ...);
  • Тип_результата — некоторый существующий (например, встроенный) тип данных или ключевое слово void,  указывающее на то что функция никакого значения возвращать не будет.
  • Имя_функции — уникальный для данного пространства имён идентификатор.
  • Тип_парN — некоторый существующий (например, встроенный) тип данных для N-oro аргумента.

Примеры:

int max (int, int);
double cube (double)
float massa();
void printarr(*int, int);

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

Если функция не возвращает никакого результата, т. е. объявлена как void, ее вызов не может быть использован как операнд более сложного выражения (например, значение такой функции нельзя чему-то присвоить).

Определение (описание) функции

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

Схема:

Тип_результата Имя_функции (Тип_пар1 Имя_пар1, Тип_пар2 Имя_пар2, ...) {
  Оператор1;
  Оператор2;
  ...
  ОператорN;
  return n;
};
  • Имя_парN — уникальное внутри функции имя N-ro параметра. Имена параметров можно задавать и в прототипе функции, тогда в определении надо использовать те же имена.
  • ОператорN — некоторые операторы и выражения, содержащиеся внутри функции и выполняющиеся каждый раз при вызове функции. Внутри операторов мы можем обращаться к глобальным объектам программы; к локальным объектам, объявленным внутри функции; а также к аргументам функции.
  • return n — оператор, останавливающий работу функции и возвращающий n в качестве её значения (при этом тип n должен соответствовать типу результата в объявлении функции). Наличие этого оператора обязательно для функции возвращающей значение. Для функции объявленной как void можно вызывать оператор return без аргументов, это досрочно завершит функцию, иначе — будут выполнены все действия до конца блока описания функции.

Блок определения функции называется также её телом.

Одна функция не может объявляться или определяться внутри другой (т.е. нельзя объявлять и определять функции внутри main).

Пример объявления и описания функции:

int max (int, int);
int max (int n1,int n2) {
  if(nl > n2) {
    return n1;
  } else {
    return n2;
  }
}

int main(void) {
  int а = 100 - max(10,20);
  cout << a;
  return 0;
}

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

Пример:

double cube (double a) {
  return a*a*a;
}

int main(void) {
  double pi = 3.1415;
  cout << cube(pi);
  return 0;
}

Но отдельно прототип указывают в тех случаях, когда функция будет описываться позже своего использования. Например, можно было объявить функцию до main, вызвать её из main, но описать только после main.

Пример:

double cube (double);
int main(void) {
  double pi = 3.1415;
  cout << cube(pi);
  return 0;
}

double cube (double a) {
  return a*a*a;
}

Такая возможность позволяет создавать модульные программы, исходный код которых может храниться даже в разных файлах.

Формальные и фактические параметры

Формальные параметры существуют в прототипе и теле определения функции. Они задаются некоторыми уникальными именами и внутри функции доступны как локальные переменные.

Фактические параметры существуют в основной программе. Они указываются при вызове функции на месте формальных.

В момент вызова функции значения фактических параметров присваиваются формальным. Соответственно, имена формальных и фактических параметров могут совпадать, это не вызовет конфликта.

Пример:

int n = -25; // глобальная переменная
int modul (int n) { // n - формальный параметр
  if(n<0) n = -1 * n; // n будет перекрывать глобальную переменную с именем n
  return n;
}

int main(void) {
  cout << modul(n); // 25, значение глобальной переменной n будет передано в функцию
  cout << n; // -25, но работа внутри функции пойдёт с локальной переменной n
  return 0;
}

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

Очередность вызова и рекурсия

Одна функция может вызываться внутри другой.

В частности, внутри своего тела функция может вызывать саму себя. Такое явление называется рекурсией.

Возможна и сложная (вложенная) рекурсия когда, например, функция А вызывает функцию В, а функция В — функцию А.

Пример простой рекурсии (функция вызывает саму себя):

int fib(int n) { 
  if(n == 1 || n == 2) {
    return 1;
  } else {
    return fib(n-1) + fib(n-2);
  }
}

int main(void) {
  cout << fib(10); // 55
  return 0;
}

Основной недостаток рекурсии — повторные вычисления одних и тех же значений.

Пример: чтоб вычислить пятое число Фибоначчи по рекурсивному алгоритму, надо вычислить четвёртое и третье числа Фибоначчи. Чтобы вычислить четвёртое — надо вычислить второе и третье, хотя третье мы уже считали для вычисления пятого.

Кроме того, в общем случае, в один момент времени может выполняться только одна функция, пока она не вернёт результат — не начнётся выполнение следующей функции. Управление от основной программы передаётся в функцию, пока функция не завершит свою работу, управление не вернётся к основной программе. В частности: ошибка в функции остановит всю программу.

Способы передачи параметров в функцию

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

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

  1. Из тела функции нельзя обратиться к какому-либо объекту, если он не является глобальным по отношению к функции или если его имя перекрыто одноимённой локальной переменной.
  2. При передаче больших объектов происходит их копирование и часто память расходуется напрасно.

Для снятия таких ограничений существует возможность передачи параметра по ссылке.

В этих случая в функцию передаётся адрес объекта и, соответственно, работа внутри функции происходит не с копией, а с оригиналом объекта.

Чтобы параметр передавался по ссылке, достаточно в прототипе функции поставить знак & после типа параметра.

Пример:

void func1(int val, int& ref) {
   val++;
   ref++;
}

...

int a = 10, b = 10;
func1(a,b);
cout << a << endl; // 10, значение будет увеличено, но внутри функции, как локальное
cout << b << endl; // 11, будет увеличено значение внешней переменной b

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

void pow2(int &a) {
    a = a*a; // тут не создаётся никаких объектов, кроме хранилища адреса
}

...

int a = 10;

pow2(a);
cout << a << endl; // 100

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

void decr(const int& n) {
   cout << n; // читать можно
   n--; // изменять нельзя, тут будет ошибка компиляции
}

В функцию можно передавать и указатели.

void prarr(int* a, const int& n) { // массив не копируется
    for(int i = 0; i<n; i++) {
        cout << a[i] << ' ';
    }
    *a = 100; // изменит первый элемент в глобальном массиве
    a++; // не вызовет ошибки, ведь указатель не константный
}

...

int a[] = {0, 5, 10, 15, 20};
prarr(a, 5); // 0 5 10 15 20
cout << *a << endl; // 100
a++; // вызовет ошибку

В теле функции указатель a — это локальный объект, но он хранит адрес первого элемента глобального массива после входа в функцию.

Массив можно было передать в функцию и по-другом. Пример прототипа:

void prarr(int a[], const int& n);

При этом указатель a внутри функции не изменит своих свойств (будет локальной переменной, хранящей адрес глобального объекта).

Корректнее параметр, по которому будет передаваться указатель на первый элемент массива, объявлять как константный:

void prarr(int* const a, const int& n);

Тогда внутри функции мы не могли бы сместить указатель на соседний элемент.

Параметры со значениями по умолчанию (необязательные параметры)

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

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

Пример:

int pow(const int& a, const int& p) {
    int res = 1;
    for(int i = 1; i<=p; i++) {
        res = res * a;
    }
    return res;
}

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

cout << pow(5,2);

Но мы могли бы объявить функцию с параметром по умолчанию таким образом:

int pow(const int& a, const int& p = 2) {
...
}

Теперь можно вызывать функцию не указывая последнего параметра:

cout << pow(5); // 25

Что равносильно вызову:

cout << pow(5,2); // 25

Или указывая его:

cout << pow(5,4); // 625

У функции может быть несколько параметров по умолчанию. Например, рассмотрим прототип функции, которая могла бы копировать из одной строки в другую начиная с i-го символа и заканчивая n-ым:

void stringcopy(char* const from, char* to, int i = 0, int n = 99);

Эту функцию можно вызывать с двумя, тремя или четырьмя параметрами. Если вызов будет с 2 параметрами, то скопируются первые 100 символов.

Если с тремя, то мы сможем указать с какого символа начать копирование, но продолжится оно до 100-го символа. Если укажем все четыре параметра, то сможем скопировать, например, символы с 30-го по 50-й.

Если мы укажем 3 параметра при вызове, то первые два значения будут отнесены к обязательным параметрам, а третье — к первому необязательному. Значения необязательным параметрам присваиваются в порядке их следования, слева направо. Нет возможности сохранив значение по умолчанию третьего параметра, как-то передать значения последнему.

Перегрузка функций

Уникальность функции определяется не только её именем, но и набором её параметров и типом возвращаемого значения. C++ позволяет создавать функцию с именем ранее существовавшей при условии, что у новой функции будет иной набор параметров.

Пример:

void printarr(int* const a, const int& n) {
    for(int i = 0; i<n; i++) cout << a[i] << ' ';
}
void printarr(char* const a) {
    cout << a;
}

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

Перегрузка функций удобна для выполнения аналогичных действий для значений разных типов:

double frac(int a, int b) {
    return (double)a/b;
}

double frac(double a, double b) {
    return a/b;
}

...

cout << frac(7,3) << endl; // работает первая функция
cout << frac(7.5,2.5) << endl; // работает вторая функция

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

А вот такой вызов вызовет ошибку компиляции:

cout << frac(3.14,2) << endl;

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

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

Пример:

double frac(int a, int b, double c = 3.14) {
    return (double)(a/b)*c;
}

Данная функция хоть и является очередной перегрузкой функции frac не может быть использована, потому что в случае вызова frac(4,3) — не понятно, какой вариант использовать: первый или третий.

Шаблоны функций

Шаблоны — средство языка C++, предназначенное для кодирования обобщённых алгоритмов, без привязки к некоторым параметрам (например, к типам данных).

Пример:

template <typename T>
void exchange ( T &a, T &b )  {
  T t;
  t = a;
  a = b;
  b = t;
}

Теперь вызвать функцию можно так:

int a=5, b=8;
exchange (a,b);
cout << a;

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

double p=3.14, e=2.71;
exchange <double> (p,e);

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

Задачи

  1. Создать функцию, которая будет иметь два целочисленных параметра a и b, и в качестве своего значения возвращать случайное целое число из отрезка [a;b]. C помощью данной функции заполнить массив из 20 целых чисел и вывести его на экран.
  2. Создать функцию, которая будет выводить указанный массив указанной длины на экран в строку. С помощью функций из 1-ой и 2-ой программ заполнить 5 массивов из 10 элемнетов каждый случайными числами и вывести все 5 массивов на экран, каждый на отдельной строке.
  3. Создать программу, которая с помощью рекурсии будет вычислять факториал числа, введённого пользователем с клавиатуры.
  4. Создать функцию, которая будет сортировать указанный массив указанной длины любым известным вам методом.
  5. Пользователь вводит с клавиатуры 7 строк длиной не более 100 символов. Создать программу, которая отсортирует и выведет на экран строки в алфавитном порядке. Например, пользователь ввёл:
    Пушкин
    Лермонтов
    Некрасов
    Толстой Л. Н.
    Толстой А. Н.
    Есенин
    Паустовский

    Программа должна вывести на экран:

    Есенин
    Лермонтов
    Некрасов
    Паустовский
    Пушкин
    Толстой А. Н.
    Толстой Л. Н.