Создаём компьютерную игру
eng   рус

Функции

Предыдущий урок: Указатели и ссылки
Следующий урок: C++ Структуры. Пользовательские типы в С++

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

Определение функции (Function Definition)

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

#include <iostream> int main() { std::cout << "Hello World!\n"; return 0; }

Здесь мы видим определение функции main. Определение состоит из заголовка (function header) и тела (function body).

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

Вызов функций (Function Call)

Когда пользователь запускает вашу программу, пусть это будет hello.exe, то операционная система вызывает функцию main - выполняется код внутри тела main. В конце тела мы возвращаем значение 0 - это значение получит вызывающее окружение - тот кто вызвал данную функцию, в данном случае - операционная система. Функция main называется точкой входа (entry point) в программу, так как она вызывается операционной системой. Точка входа для оконной программы Windows будет выглядеть по-другому.

В нашей программе может быть сколько угодно функций. Давайте создадим ещё одну и посмотрим как можно самим вызывать функции:

#include <iostream> int move() { std::cout << "Персонаж переместился\n" ; return 0; } int main() { move(); move(); move(); return 0; }
На экране строка будет выведена три раза. move(); - вызов функции move. Во-первых, заметьте, что определение move идёт до main. Определение всегда должно идти перед вызовом. Вызов функции состоит из имени функции и списка аргументов в круглых скобках. Функция может ничего не возвращать, мы можем переписать move следующим образом:
void move() { std::cout << "Персонаж переместился\n" ; return; } void move() { std::cout << "Персонаж переместился\n" ; }

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

Пока что функция move говорит нам что персонаж куда-то переместился, давайте уточним координаты:

#include <iostream> void move(int x, int y) { std::cout << "Персонаж переместился в точку (" << x << ", " << y << ")\n"; } int main() { move(0, 0); move(1, 5); move(99, 20); return 0; }

Теперь в списке аргументов у нас два параметра x и y с типом int. Внутри функции они ведут себя как обычные переменные (это и есть обычные переменные, просто определены они необычно). Теперь самое важное: параметры получают своё значение (инициализируются) при вызове функции. Когда мы вызываем функцию, в круглых скобках мы передаём аргументы. Аргументы соответствуют параметрам: первый аргумент - первому параметру, второй - второму... Типы аргументов и параметров должны совпадать, т.е. если параметр имеет тип int, то соответствующий аргумент не может быть float.

Передача аргументов по значению (pass-by-value)

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

#include <iostream> void move(int x) { std::cout << "Персонаж переместился в точку " << x << ")\n"; } int main() { int x = 5; move(x); return 0; }

Смотрите, у нас есть две переменные x - одна в функции main, а другая в функции move. Разберём все действия компьютера по порядку. При запуске обе функции загружаются в память. У каждой функции есть адрес - это начало тела функции (первая инструкция функции). Также, при запуске создаётся две области памяти: куча (heap) и стек (stack). Операционная система вызывает функцию main (точку входа). В первой строке main создаётся переменная x и она размещается в стеке. На следующей строке происходит вызов функции move - процессор переходит к её началу. При этом происходит копирование аргумента в параметр, в данном случае - переменной x из main в параметр x функции move. Это две разные переменные с разными адресами. Для параметра память выделяется тоже в стеке. Далее происходит выполнение всех инструкций функции move. После того как функция move закончилась, процессор возвращается к инструкциям функции main и продолжает выполнение оператора, стоящего после вызова move.

Т.е. при вызове функции все аргументы копируются в новые переменные. Это называется передачей аргументов по значению (pass-by-value). После окончания функции переменные из стека удаляются. Давайте взглянем ещё на один пример:

void move(int x) { int y = 10; std::cout << "Персонаж переместился в точку " << x << ")\n"; } move(1); move(2); move(3);

x - параметр, y - переменная созданная внутри функции. y также будет размещена в стеке. После окончания функции все переменные из стека будут удалены. Здесь происходит три вызова move. Каждый раз будет происходить создание x и y вначале функции и их удаление в конце. Переменные, созданные внутри какой-либо функции называются локальными.

Область видимости (Scope)

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

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

int v = 5; // глобальная переменная int main () { // код return 0; }

здесь переменная v является глобальной и к ней можно будет получить доступ в любое время пока работает программа.

Передача аргументов по указателю (pass-by-pointer) и по ссылке (pass-by-reference)

Продолжим работать с перемещением персонажа. Давайте попробуем поменять в функции значение передаваемых переменных. Как мы видели выше мы не можем изменить значение аргумента через параметр, так как это две разные переменные (в разных участках памяти). Но мы можем передать указатель, тогда в параметр будет копироваться адрес. Это называется передачей по указателю (pass-by-pointer).

#include <iostream> void move(int* x, int* y, int dx, int dy) { *x += dx; *y += dy; std::cout << "Персонаж переместился в точку (" << *x << ", " << *y << ")\n"; } int main() { int x(0); int y(0); move(&x, &y, 1, 5); move(&x, &y, 2, 2); move(&x, &y, 3, -1); return 0; }

dx и dy отображают насколько единиц персонаж переместился (d означает дельта - это стандартное значение используемое для обозначения изменений). В функции main мы создаём x, y. В move мы передаём адреса и насколько нужно изменить координаты. Первые два аргумента - адреса, а соответствующие им параметры - указатели на эти адреса.

Ещё один вариант передачи - передача по ссылке (pass-by-refrence). Также как и передача по указателю это позволяет изменять внешние переменные внутри функции. Давайте посмотрим пример:
#include <iostream> void move(int& x, int& y, int dx, int dy) { x += dx; y += dy; std::cout << "Персонаж переместился в точку (" << x << ", " << y << ")\n"; } int main() { int x(0); int y(0); move(x, y, 1, 5); move(x, y, 2, 2); move(x, y, 3, -1); return 0; }

Всё примерно также как и с указателями.

Передача в функцию массива

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

#include <iostream> void sort(int a[]) { // сортировка } int main() { int a[5] = { 3, 1, 5, 4, 2 }; sort(a); return 0; }

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

Инициализация параметров функций

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

void move(int& x, int& y, int dx = 1, int dy = 1) { // код } // ... move(x, y); // будут использоваться значения по умолчанию move(x, y, 2, 2); // будут использоваться аргументы move(x, y, 5); // dx = 5, dy = 1

Прототипы (Prototypes)

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

#include <iostream> void move(int x); // прототип int main() { int x = 5; move(x); return 0; } void move(int x) { std::cout << "Персонаж переместился в точку " << x << ")\n"; }

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

Возвращаемое значение

С возвращаемыми значениями не должно быть сложностей. Тип возвращаемого значения должен соответствовать значению после return. Единственное, никогда не возвращайте локальные переменные. Часто возвращаются указатели на выделенную память или результаты какого-либо вычисления.

Статичные переменные (Static Variables)

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

void count (){ static int counter (0); counter++; } count(); count(); count();

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

Заключение

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

Комментарии:

No comments yet