PVOID - Функции

Функции

Этот раздел является переводом туториала C++ Language


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

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

type name ( parameter1, parameter2, ...) { statements }

 Где:

  • type - тип значения, возвращаемого функцией;

  • name - идентификатор, по которому функция может быть вызвана;

  • parameters (столько, сколько нужно): Каждый параметр состоит из типа, за которым следует идентификатор, каждый параметр отделяется от следующего запятой. Каждый параметр выглядит выглядит очень похожим на обычное объявление переменной (например, int x), и фактически действует внутри функции как обычная переменная, которая является локальной для функции. Назначением параметров является возможность передачи аргументов в функцию из того места, откуда она вызывается;

  • statements - это тело функции. Это блок операторов, заключенных в фигурные скобки { }, которые определяют, что на самом деле делает функция.

  Давайте рассмотрим пример:

// пример функции
#include <iostream>
using namespace std;

int addition (int a, int b)
{
  int r;
  r=a+b;
  return r;
}

int main ()
{
  int z;
  z = addition (5,3);
  cout << "The result is " << z;
}
The result is 8

   Эта программа разделена на две функции: addition и main. Помните, что независимо от порядка определения функция, программа на C++ всегда начинается с вызова main. Фактически, main является единственной функцией, вызываемой автоматически, и код любой другой функции выполняется только в том случае, когда эта функция вызывается из main (прямо или косвенно).

   В приведенном примере, main начинается с объявления переменной z типа int, и сразу за этим она производит первый вызов функции: она вызывает addition. Вызов функции следует структуре, очень похожей на ее объявление. В приведенном выше примере вызов сложения можно сравнить с его определением несколькими строками ранее:

   Параметры в объявлении функции полностью соответствуют параметрам при вызове функции. Вызов передает два значения, 3 и 5, в функцию; они соответствуют параметрам a и b, объявленных для функции addition.

   В точке, в которой функция вызывается из main, управление передается функции addition: здесь выполнение main останавливается и возобновляется только после завершения функции addition. В момент вызова функции значения обоих аргументов (5 и 3) копируются в локальные переменные int a и int b внутри функции.

   Затем, внутри addition, объявляется другая локальная переменная (int r), и с помощью выражения r = a + b, результат a + b присваивается переменной r; что для данного случая, когда a равна 5, b равна 3, значит, что переменной r будет присвоено значение 8.

 Последний оператор в теле функции:

return r;

  завершает функцию addition и возвращает управление обратно в точку вызова функции; в данном случае: в функцию main. Точно в этот момент программа передает управление в main, возвращаясь точно в ту же самую точку, в которой она была прервана вызовом addition. Дополнительно, ввиду того, что addition имеет возвращаемый тип, вызов оценивается как имеющий значение, и этим значением является значение, указанное в операторе return, который завершает addition: в данном конкретном случае, значение локальной переменной r, которое на момент выполнения оператора return имеет значение 8.

   Следовательно, вызов addition является выражением со значением, возвращаемым функцией, в данном случае, это значение, 8, присваивается переменной z. Это как если бы весь вызов функции (addition(5,3)) был замещен значением, которое она возвращает (т.е. 8).

 Затем main просто печатает это значением путем вызова:

cout << "The result is " << z;

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

// пример функции
#include <iostream>
using namespace std;

int sub (int a, int b)
{
  int r;
  r=a-b;
  return r;
}

int main ()
{
  int x=5, y=3, z;
  z = sub (7,2);
  cout << "The first result is "  << z << '\n';
  cout << "The second result is " << sub (7,2) << '\n';
  cout << "The third result is "  << sub (x,y) << '\n';
  z= 4 + sub (x,y);
  cout << "The fourth result is " << z << '\n';
}
The first result is 5
The second result is 5
The third result is 2
The fourth result is 6

   Подобно функции addition в предыдущем примере, этот пример определяется функцию sub, которая просто возвращает разницу между двумя ее параметрами. На этот раз, main вызывает функцию несколько раз, демонстрируя больше возможных способов вызова функции.

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

z = sub (7,2);
cout << "The first result is " << z;

   Если мы заменим вызов функции на значение, которое она возвращает (т.е. 5), мы получим:

z = 5;
cout << "The first result is " << z;

 Пользуясь такой же процедурой, мы можем интерпретировать:

cout << "The second result is " << sub (7,2);

 как:

cout << "The second result is " << 5;

 потому что 5 - это значение, возвращаемое sub (7,2).

 В случае:

cout << "The third result is " << sub (x,y);

 аргументы, передаваемые в sub являются переменными, а не литералами. Это так же корректно и работает нормально. Функция вызывается со значениями x и y, равными на момент вызова: 5 и 3 соответственно, возвращая 2 как результат.

 Четвертый вызов также похож:

z = 4 + sub (x,y);

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

z = sub (x,y) + 4;

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

z = 4 + 2; // эквивалентно z = 4 + sub (x,y);
z = 2 + 4; // эквивалентно z = sub (x,y) + 4; 

 

 Функции без типа. Использование void.


  Синтаксис, показанный выше для функций:

type name ( argument1, argument2 ...) { statements }

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

// пример функции с void
#include <iostream>
using namespace std;

void printmessage ()
{
  cout << "I'm a function!";
}

int main ()
{
  printmessage ();
}
I'm a function!

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

void printmessage (void)
{
  cout << "I'm a function!";
}

   В C++ может быть использован пустой список параметров вместо void с тем же значением, но использование void в списке аргументов было популяризировано языком Си, где это является требованием.

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

printmessage ();

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

printmessage;

 

 Значение, возвращаемое функцией main


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

   Что ж здесь есть подвох: если исполнение main завершается нормально, не встречая оператор return, компилятор предполагает, что функция завершается с неявным оператором return:

return 0;

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

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

 значение   описание 
 0  Программа завершена успешно 
 EXIT_SUCCESS   Программа завершена успешно (то же, что и предыдущее). 
 Это значение определено в заголовочном файле <cstdlib>.
 EXIT_FAILURE   Программа завершена с ошибкой.
 Это значение определено в заголовочном файле <cstdlib>.

по той причине, что неявный оператор return 0; для main является хитрым исключением, некоторые авторы считают, что явное написание этого оператора является хорошей практикой.

 

 Аргументы, передаваемые по значению и по ссылке


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

int x=5, y=3, z;
z = addition ( x, y );

   В этом случае, в функцию addition переданы значения 5 и 3, которые являются копиями значений переменных x и y соответственно. Эти значения (5 и 3) используются для инициализации переменных, установленных в качестве параметров в определении функции, но любая модификация этих переменных внутри функции не повлияет на значения переменных x и y вне функции, потому что сами переменные x и y не были переданы в функцию при вызове, а только копии их значений на тот момент.

function arguments

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

// передача параметров по ссылке
#include <iostream>
using namespace std;

void duplicate (int& a, int& b, int& c)
{
  a*=2;
  b*=2;
  c*=2;
}

int main ()
{
  int x=1, y=3, z=7;
  duplicate (x, y, z);
  cout << "x=" << x << ", y=" << y << ", z=" << z;
  return 0;
}
x=2, y=6, z=14

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

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

   Фактически, a, b и c становятся псевдонимами аргументов, передаваемых при вызове функции (x, y и z), и любое изменение переменной a внутри функции приведет к модификации переменной x вне функции. Любое изменение переменной b модифицирует y, а любое изменение переменной c модифицирует z. Вот почему, когда в примере функция duplicate модифицирует значения переменных a, b и c, это отражается на значениях x, y и z.

 Если вместо определения функции duplicate как:

void duplicate (int& a, int& b, int& c)

 было бы определение без знаков амперсанда:

void duplicate (int a, int b, int c)

переменные передавались бы не по ссылке, а по значению, создавая вместо этого копии значений. В этом случае выходные данные программы были бы значениями x, y и z без изменения (то есть 1, 3 и 7).

 

 Вопросы эффективности и константные ссылки


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

string concatenate (string a, string b)
{
  return a+b;
}

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

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

string concatenate (string& a, string& b)
{
  return a+b;
}

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

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

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

string concatenate (const string& a, const string& b)
{
  return a+b;
}

добавлением квалификатора const функция запрещает изменять значения как a, так и b, но может получить доступ к их значениям по ссылкам (псевдонимам аргументов) без необходимости делать фактические копии строк.

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

 

Встроенные (inline) функции


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

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

 Например, описанная функция конкатенации может быть объявлена встроенной как:

inline string concatenate (const string& a, const string& b)
{
  return a+b;
}

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

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

 

 Значения по умолчанию в параметрах


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

// значения по умолчанию в функциях
#include <iostream>
using namespace std;

int divide (int a, int b=2)
{
  int r;
  r=a/b;
  return (r);
}

int main ()
{
  cout << divide (12) << '\n';
  cout << divide (20,4) << '\n';
  return 0;
}
6
5

 В этом примере два вызова функции divide. В первом:

divide (12)

вызов передает только один аргумент в функцию, хотя функция имеет два параметра. В этом случае функция предполагает, что второй параметр должен быть 2 (обратите внимание на определение функции, где второй параметр объявляется как int b = 2). Следовательно, результат будет 6.

   Во втором случае:

divide (20,4)

вызов передает два аргумента в функцию. Следовательно, значение по умолчанию для b (int b=2) игнорируется, и b принимает значение переданного аргумента, равное 4, что дает результат 5.

 

 Объявление функций


   В C++ идентификаторы могут использоваться в выражениях только после того, как они были объявлены. Например, некоторая переменная x не может быть использована перед объявлением с помощью оператора, например:

int x;

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

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

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

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

int protofunction (int first, int second);
int protofunction (int, int);

   В любом случае, включение имени для каждого параметра всегда улучшает читаемость объявления.

// объявление прототипов функций
#include <iostream>
using namespace std;

void odd (int x);
void even (int x);

int main()
{
  int i;
  do {
    cout << "Enter number (0 to exit): ";
    cin >> i;
    odd (i);
  } while (i!=0);
  return 0;
}

void odd (int x)
{
  if ((x%2)!=0) cout << "It is odd.\n";
  else even (x);
}

void even (int x)
{
  if ((x%2)==0) cout << "It is even.\n";
  else odd (x);
}
Enter number (0 to exit): 9
It is odd.
Enter number (0 to exit): 6
It is even.
Enter number (0 to exit): 1030
It is even.
Enter number (0 to exit): 0
It is even.

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

 следующие строки:

void odd (int a);
void even (int a);

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

   Но объявление функций перед определением полезно не только для реорганизации порядка функций в коде. В некоторых случаях, например в данном конкретном случае, требуется как минимум одно из объявлений, потому что odd и even являются взаимно вызываемыми; в odd есть вызов even, а в even есть вызов odd. Следовательно, нет способа структурировать код так, чтобы определить odd перед even и even перед odd одновременно.

 

 Рекурсивность


   Рекурсивность — это свойство функции вызывать саму себя. Это полезно для некоторых задач, таких как сортировка элементов или вычисление факториала чисел. Например, чтобы получить факториал числа (n!), Математическая формула была бы:

 n! = n * (n-1) * (n-2) * (n-3) ... * 1
 Более конкретно 5! (факториал 5) будет:

 5! = 5 * 4 * 3 * 2 * 1 = 120
 Рекурсивная функция для вычисления этого в C++ может быть:

// вычисление факториала
#include <iostream>
using namespace std;

long factorial (long a)
{
  if (a > 1)
   return (a * factorial (a-1));
  else
   return 1;
}

int main ()
{
  long number = 9;
  cout << number << "! = " << factorial (number);
  return 0;
}
9! = 362880

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