Добавил:
Преподаватель Колледжа информационных технологий Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Лекции / ФУНКЦИИ

.pdf
Скачиваний:
41
Добавлен:
08.05.2022
Размер:
1.19 Mб
Скачать

// Делаем что-нибудь с array

delete[] array;

return 0;

}

Здесь не возникнет никаких проблем, так как динамически выделенная память не выходит из области видимости в конце блока, в котором объявлена и всё еще будет существовать, когда адрес будет возвращаться в caller.

Когда использовать возврат по адресу:

при возврате динамически выделенной памяти;

при возврате аргументов функции, которые были переданы по

адресу.

Когда не использовать возврат по адресу:

при возврате переменных, которые были объявлены внутри функции (используйте возврат по значению);

при возврате большой структуры или класса, который был передан по ссылке (используйте возврат по ссылке).

Подобно передаче по ссылке, значения, возвращаемые по ссылке,

должны быть переменными (вы не сможете вернуть ссылку на литерал или выражение). При возврате по ссылке в caller возвращается ссылка на переменную. Затем caller может её использовать для продолжения изменения переменной, что может быть иногда полезно. Этот способ также очень быстрый и при возврате больших структур или классов.

Однако, как и в возврате по адресу, вы не должны возвращать локальные переменные по ссылке. Рассмотрим следующий фрагмент кода:

int& doubleValue(int a)

{

int value = a * 3;

return value; // value возвращается по ссылке здесь

} // value уничтожается здесь

21

В программе, приведенной выше, возвращается ссылка на переменную value, которая уничтожится, когда функция завершит свое выполнение. Это означает, что caller получит ссылку на мусор. Компилятор, вероятнее всего,

выдаст предупреждение или ошибку, если вы попытаетесь это сделать.

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

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

 

Листинг 7.14.

 

 

 

1

#include <iostream>

 

2

#include <array>

 

3

using namespace std;

 

4

int& getElement(std::array<int, 20> &array, int index)

 

5

{

 

6

return array[index];

 

7

}

 

8

int main()

 

9

{

 

10

array<int, 20> array;

 

11

getElement(array, 15) = 7;

 

12

cout << array[15] << '\n';

 

13

return 0;

 

14

}

 

 

Результат выполнения программы:

 

7

 

Когда мы вызываем getElement(array, 15), то getElement()

возвращает ссылку на элемент массива под индексом 15, а затем main()

использует эту ссылку для присваивания этому элементу значения 7.

Когда использовать возврат по ссылке:

при возврате ссылки-параметра;

при возврате элемента массива, который был передан в функцию;

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

Когда не использовать возврат по ссылке:

22

при возврате переменных, которые были объявлены внутри функции (используйте возврат по значению);

при возврате стандартного массива или значения указателя

(используйте возврат по адресу).

§7.10 Указатели на функции

Указатель на функцию (function pointer) хранит адрес функции. По сути указатель на функцию содержит адрес первого байта в памяти, по которому располагается выполняемый код функции.

Самым распространенным указателем на функцию является ее имя. С

помощью имени функции можно вызывать ее и получать результат ее работы.

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

тип (*имя_указателя) (параметры)

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

имя_указателя представляет произвольно выбранный идентификатор

всоответствии с правилами о наименовании переменных.

Ипараметры определяют тип и название параметров через запятую при их наличии.

Например, определим указатель на функцию:

void (*message) ()

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

(то есть ничего не возвращают).

Используем указатель на функцию:

Листинг 7.15.

1#include <iostream>

2using namespace std;

3void hello();

4void goodbye();

5int main()

6{

23

7

void (*message)();

8

message=hello;

9

message();

10

message = goodbye;

11

message();

12

return 0;

13

}

14

void hello()

15

{

16

cout << "Hello, World" << endl;

17

}

18

void goodbye()

19

{

20

cout << "Good Bye, World" << endl;

21

}

Указателю на функцию можно присвоить функцию, которая соответствует указателю по возвращаемому типу и спецификации параметров:

message=hello;

То есть в данном случае указатель message теперь хранит адрес функции hello. И посредством обращения к указателю мы можем вызвать эту функцию:

message();

В качестве альтернативы мы можем обращаться к указателю на функцию следующим образом:

(*message)();

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

Рисунок 7.1

При определении указателя стоит обратить внимание на скобки вокруг имени. Так, использованное выше определение

24

void (*message) ();

НЕ будет аналогично следующему определению:

void *message ();

Во втором случае определен не указатель на функцию, а прототип функции message, которая возвращает указатель типа void*.

Рассмотрим еще один указатель на функцию:

Листинг 7.16.

1

#include <iostream>

 

2

using namespace std;

 

3

int add(int, int);

 

4

int subtract(int, int);

 

5

int main()

 

6

{

 

7

int a = 10;

 

8

int b = 5;

 

9

int result;

 

10

int (*operation)(int a, int b);

 

11

operation=add;

 

12

result = operation(a, b);

 

13

cout << "result=" << result << endl;

// result=15

14

operation = subtract;

 

15

result = operation(a, b);

 

16

cout << "result=" << result << endl;

// result=5

17

return 0;

 

18

}

 

19

int add(int x, int y)

 

20

{

 

21

return x+y;

 

22

}

 

23

int subtract(int x, int y)

 

24

{

 

25

return x-y;

 

26

}

 

Здесь определен указатель operation, который может указывать на функцию с двумя параметрами типа int, возвращающую также значение типа int. Соответственно мы можем присвоить указателю адреса функций add и

25

subtract и вызвать их, передав при вызове указателю некоторые значения для параметров.

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

тип (*имя_массива[размер]) (параметры)

Например:

double (*actions[]) (int, int)

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

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

Листинг 7.17.

1

#include <iostream>

 

2

using namespace std;

 

3

void add(int, int);

 

4

void subtract(int, int);

 

5

void multiply(int, int);

 

6

int main()

 

7

{

 

8

int a = 10;

 

9

int b = 5;

 

10

void (*operations[3])(int, int) = {add, subtract,

multiply};

 

11

// получаем длину массива

12

int length = sizeof(operations)/sizeof(operations[0]);

13

for(int i=0; i < length;i++)

14

{

 

15

operations[i](a, b);

// вызов функции по указателю

16

}

 

17

return 0;

 

18

}

 

19

void add(int x, int y)

 

20

{

 

21

cout << "x + y = " << x + y << endl;

22

}

 

23

void subtract(int x, int y)

24

{

 

 

 

26

25

int result = x - y;

26

cout << "x - y = " << x - y << endl;

27

}

28

void multiply(int x, int y)

29

{

30

cout << "x * y = " << x * y << endl;

31

}

Здесь массив operations содержит три функции add, subtract и multiply, которые последовательно вызываются в цикле через перебор массива в функции main.

Консольный вывод программы представлен на Рисунке 7.2.

Рисунок 7.2

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

качестве параметра. Например:

Листинг 7.18.

1

#include <iostream>

2

using namespace std;

3

int add(int, int);

4

int subtract(int, int);

5

int operation(int(*)(int, int), int, int);

6

int main()

7

{

8

int a = 10;

9

int b = 5;

10

int result;

11

result = operation(add, a, b);

12

cout << "result: " << result << std::endl;

13

result = operation(subtract, a, b);

14

cout << "result: " << result << std::endl;

15

return 0;

16

}

17

int add(int x, int y)

18

{

19

return x + y;

 

27

20

}

21

int subtract(int x, int y)

22

{

23

return x - y;

24

}

25

int operation(int(*op)(int, int), int a, int b)

26

{

27

return op(a, b);

28

}

В данном случае первый параметр функции operation - int

(*op)(int, int) - представляет указатель на функцию, которая возвращает значение типа int и принимает два параметра типа int. Результатом функции является вызов той функции, на которую указывает указатель.

Определению указателя соответствуют две функции: add и subtract,

поэтому их адрес можно передать в вызов функции operation:

operation(add, a, b);.

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

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

Рассмотрим простейший пример:

Листинг 7.19.

1

#include <iostream>

2

using namespace std;

3

void goodmorning();

4

void goodevening();

5

void(*message(int))();

6

int main()

 

7

{

 

8

void(*action)();

// указатель на выбранную функцию

9

action = message(15);

10

action();

// выполняем полученную функцию

11

return 0;

 

12

}

 

13

void(*message(int hour))()

14

{

 

 

 

28

15 if (hour > 12)

16 return goodevening;

17 else

18 return goodmorning;

19 }

20 void goodmorning()

21 {

22 cout << "Good Morning!" << endl;

23 }

24 void goodevening()

25 {

26 cout << "Good Evening!" << endl;

27 }

Здесь определена функция message, которая в зависимости от переданного числа возвращает одну из двух функций goodmorning или goodevening. Рассмотрим объявление функции message:

void(*message(int hour))()

Вначале указан тип, который возвращается функцией, которая возвращается из message, то есть тип void (функции goodmorning и goodevening имеют тип void). Далее идет в скобках имя функции со списком параметров, то есть функция message принимает один параметр типа int:

(*message(int hour)). После этого отдельно в скобках идет спецификация параметров функции, которая будет возвращаться из message. Поскольку функции goodmorning и goodevening не принимают никаких параметров,

то указываются пустые скобки.

Имя функции фактически представляет указатель на нее, поэтому в функции message мы можем возвратить нужную функцию, указав после оператора return ее имя.

Для получения указателя на функцию определяем переменную action:

void(*action)();

Эта переменная представляет указатель на функцию, которая не принимает параметров и имеет в качестве возвращаемого типа тип void, то есть она соответствует функциям goodmorning и goodevening.

29

Затем вызываем функцию message и получаем указатель на функцию в переменную action:

action = message(15);

Далее, используя указатель action, вызываем полученную функцию:

action();

Поскольку в функцию message передается число 15, то она будет возвращать указатель на функцию goodevening, поэтому при ее вызове на консоль будет выведено сообщение "Good Evening!".

§7.11 Рекурсия

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

Такая функция называется рекурсивной (recursive function).

Во-первых, нужно уяснить, что рекурсия — это, по сути, перебор.

Задачи, которые можно решить итеративно, можно решить и рекурсивно,

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

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

Идём дальше. Так же, как и у цикла (перебора), рекурсия должна иметь условие остановки, называемое базовым случаем. Иначе и цикл, и рекурсия будут работать бесконечно. Условие остановки определяет шаг рекурсии.

Рекурсивная функция вызывается, пока не произойдёт остановка рекурсии (не сработает базовое условие и не произойдёт возврат к последнему вызову функции). Именно поэтому решение задачи на рекурсию сводится к решению базового случая.

Если мы решаем сложную задачу (небазового случая), мы выполняем несколько рекурсивных вызовов либо шагов, т. к. наша цель — упростить задачу. И делать это до тех пор, пока не придём к базовому решению.

Ещё раз. Рекурсивная функция включает в себя:

базовый случай, он же условие остановки;

30