12 января 2010 в 19:05
Функции в C++. Часть вторая.
Передача массивов в функцию
Передача массивов в функцию имеет небольшое отличие от передачи простой переменной. В прототипе функции необходимо указывать идентификатор:
int simple_function (int array[]);
int simple_function (int array[]) { return 0;}
Обратите внимание, что рядом с идентификатором и в объявлении и в определении стоят скобочки.
При вызове функции, в которую нужно передать массив, указывается только идентификатор массива (без квадратных скобок):
int a[] = { 0, 1, 2 };
simple_function(a);
Запомните, при вызове указывается только идентификатор, а при объявлении/определении ещё и скобки.
Глобальные переменные
Глобальные переменные - это переменные, которые определены за пределами любой функции:
int x = 0; // глобальная переменная int main () { код main } void simple_function () { x = 5; }
Здесь переменная x видна из любой функции программы, в том числе и из simple_function. Что значит: "функция видит переменную"? Это значит, что внутри функции можно обращаться к этой переменной.
Глобальные переменные существуют с того момента как они встретились в коде и до конца программы. Т.е. если глобальную переменную объявить после какой-нибудь функции, то в этой функции данную переменную нельзя будет использовать:
int main () { код main } void simple_function () { x = 5 // компилятор выдаст ошибку, переменная ещё не объявлена } int x = 5; // объявление глобальной переменной int getX () { return x; // getx видит переменную x }
Переменная x объявлена после функций main и simple_function, а значит, в этих функциях её не видно.
Локальные переменные
Локальные переменные объявлены внутри функций и видны только в них самих:
void f1 () { int a; // объявление переменной f2(); } void f2 () { a = 3; // ошибка, отсутствует объявление переменной }
У каждой функции есть своя область видимости. В область видимости функции входят все глобальные переменные и переменные объявленные в этой функции.
Механизм передачи значений через аргументы и return предназначен как раз для локальных переменных:
int x; // глобальная переменная void f1 () { int a = 5; int b = 3; int t = 1; x = 5; int c = f2(b,a); } int f2(int a, int t) { int b = 1; return a+t+x; }
Обе функции видят переменную x, т.к. она глобальная.
У каждой функции свои переменные a,b,t. У f1 a=5,b=3,t=1, а у f2 a=3,t=5,b=1. Несмотря на то, что у этих функций есть переменные с одинаковыми идентификаторами, это всё-таки совершенно разные переменные. Будьте бдительны!
Переменной c будет присвоено значение 13.
Статические локальные переменные
Обычные локальные переменные, когда функция завершается, уничтожаются. И при каждом выполнении функции, они создаются заново:
void simple_function () { int a = 0; a = a + 1; }
В данном примере, сколько бы раз не вызывалась функция, переменная a никогда не станет больше единицы.
Статические переменные определяются только один раз - когда функция вызывается в первый раз. Когда функция заканчивает выполнение операторов, статические переменные остаются в памяти. Когда функция снова вызывается, она продолжает их использовать:
int simple_function() { static int a = 0; a = a + 1; return a; } int main () { int a = 0; a = simple_function(); cout << a; // 1 a = simple_function(); cout << a; // 2 a = simple_function(); cout << a; // 3 return 0; }
Передача аргументов по значению (pass-by-value)
Рассмотрим пример:
int simple_function(int); // прототип ф-ии int main() { int x = 4; // переменная x в вызывающем окружении int y; y = simple_function(x); // возвращаемое значение ф-ии присваивается переменной y cout << x; // 4 return 0; } int simple_function(int x) // переменная x в функции { x = x + 5; return x; }
В main мы объявляем две переменные, одной присваиваем значение. Затем вызываем функцию и передаём ей переменную x. Управление передаётся функции и начинается выполнение тела ф-ии.
Обратите внимание, что в заголовке и теле функции используется переменная x. Но это не та переменная, которая объявлена в вызывающем окружении.
Когда после вызова функции мы выводим значение x, на экране появляется 4, а не 9.
Дело в том, что когда происходит вызов функции, значение аргумента копируется и в теле функции используется скопированное значение.
Запомните, две переменные: переменная-аргумент из вызывающего окружения и переменная-параметр функции - две совершенно разные переменные, хранящиеся в разных участках памяти. Если вы измените переменную в функции, то это не значит что изменится и переменная из вызывающего окружения.
Вот этот механизм и называется передачей по значению (pass-by-value). Мы передаём не саму переменную, а её значение. При этом происходит создание ещё одной переменной того же типа и в неё помещается такое же значение.
Тут вот на какой момент стоит обратить внимание: внутри функции мы не можем работать с внешними переменными (за исключением глобальных), они не видны из функции! Мы можем работать только со значениями тех, которые мы передали в функцию в виде аргументов.
Передача аргумента по ссылке (pass-by-reference)
Ссылка на переменную - это другое имя той же переменной. Т.е. это как псевдоним.
Передача по ссылке (pass-by-reference) позволяет изменять значение переменной из внешнего окружения внутри функции.
int simple_function(int&); // прототип ф-ии int main () { int x = 4; // переменная x в вызывающем окружении int y; y = simple_function(x); // присваивание переменной y возвращаемого значения функции cout << x; // 9 return 0; } int simple_function(int& z) // переменная z в функции { z = z + 5; return z; }
Для передачи по ссылке, после имени типа в обьявлении и определении нужно поставить амперсанд. Больше никаких изменений делать не нужно. Я только изменил имя переменной внутри функции для наглядности.
x и z - одна и та же переменная. При обращении к одной из этих переменных мы обращаемся к одному участку памяти. При передаче аргумента по ссылке не происходит копирования. Просто для участка памяти, где содержится значение, создаётся ещё одно имя - z. Поэтому после вызова функции, у переменной x - новое значение 9.
Через оператор return можно вернуть только одно значение. Передача по ссылке нужна, когда необходимо вернуть несколько значений. Нужно просто передать необходимое количество аргументов и внутри функции произвести над ними необходимые операции. При этом можно вообще ничего не возвращать (использовать тип void). Посмотрите на пример для наглядности:
void simple_function (int&, int&, int&); int main () { int a = 0; int b = 2; int c = 7; simple_function (a, b, c); cout << a; // 1 cout << b; // 4 cout << c; // 10 return 0; } void simple_function (int& x, int& y, int& z) { x = x + 1; y = y + 2; z = z + 3; }
Передача аргументов по указателю (pass-by-pointer)
Обычно в учебниках указатели обьясняют довольно рано и уделяют этой теме немного внимания. Я не буду сейчас объяснять эту тему подробно, потому как она очень сложная и у нас пока нет необходимости использования указателей в программах. Указатели мы рассмотрим после классов и рассмотрим очень подробно т.к. они - одно из ключевых понятий в программировании. В данном уроке заострю внимание только на одном аспекте.
Массивы всегда передаются в функции через указатели:
void simple_function (int a[]); // прототип int array[5]; simple_function(array); // передача массива в функцию
При передаче значения по указателю не происходит копирования значений. Отличие же от ссылки в том, что указатель - это не другое имя переменной, указатель - это адрес в памяти по которому можно найти значение переменной. Пока что не обращайте внимания на эти тонкости. Просто запомните, что при передаче массива в функцию, мы используем указатели. Имя массива - это и есть переменная-указатель, но если после идентификатора стоят квадратные скобки с индексом, то происходит обращение к конкретному элементу массива:
int array[5]; a = array; // указатель a = array[0]; // не указатель
Перегруженные функции
Очень часто возникает ситуация, когда несколько функций выполняют одинаковые действия, только над разными типами. В данном случае можно использовать перегруженные функции.
Перегруженные функции - это функции с одним именем, но с разными типами аргументов:
int simple_function (float a, float b);
int simple_function (int a, int b);
int simple_function (int a);
Здесь представлено три объявления перегруженной функции simple_function. Для каждого варианта функции, нужно написать своё определение.
Перегруженные функции могут отличаться не только типами аргументов, но и их количевством (сравните 2-е и 3-е объявления).
И ещё один важный момент: в перегруженных функциях учитываются только аргументы, возвращаемое значение не играет никакой роли. В следующем примере, компилятор выдаст ошибку:
int simple_function (int a);
float simple_function (int a);
Константные аргументы функции
Как мы уже знаем, при передаче аргумента по значению, мы не можем изменить переменную из вызывающего окружения. Если мы всё же хотим изменить значение этой переменной, то аргумент нужно передавать по ссылке. Но иногда возникает ситуация, когда нужно передать аргумент по ссылке и при этом запретить его изменять. Для этого можно воспользоваться константными аргументами функции. В списке аргументов объявления/определения нужно поставить ключевое слово const перед соответствующим аргументом:
int simple_function(const int&, int); int simple_function(const int& a, int b); { a = 1; // Компилятор выдаст ошибку! Нельзя изменять переменную a return 0; }
Теперь, для чего это нужно: в следующих уроках мы познакомимся с новыми типами данных. Эти типы данных в некоторых случаях могут занимать сотни байт (сравните со стандартным типом double - 8 байт). Если передавать эти аргументы в функцию по значению, то будет происходить копирование, а это значит замедление программы. Поэтому для большей эффективности мы будем передавать эти переменные по ссылкам. Ну а чтобы "случайно" не изменить их значения, будем пользоваться константными аргументами.
Встраиваемые функции (inline functions)
Вызов функции, передача в неё значений, возврат значения, эти операции занимают довольно много процессорного времени. Когда мы будем рассматривать более реальные примеры, у нас будут функции в один-два оператора (в основном - математические), которые нужно будет выполнять тысячи раз в секунду. Так как размер функции небольшой (несколько операторов), то время выполнения тела функции будет меньше чем процесс вызова/передачи аргументов/возврата значения. Чтобы сократить время работы программы, можно воспользоваться встроенными функциями.
Перед объявлением/определением функции нужно добавить ключевое слово inline.
При этом, во время компиляции все вызовы функции будут заменены непосредственно кодом функции. При этом программа будет выполняться быстрее, но займёт больше памяти (если функция вызывается много раз):
inline void simple_function() { // тело функции } int main () { simple_function(); simple_function(); simple_function(); return 0; }
Во время компиляции этот код превратится в следующий:
int main () { // тело функции // тело функции // тело функции return 0; }
Компилятор решает делать функцию встраиваемой или нет, в зависимости от размера функции. Т.е. если вы пытаетесь сделать длинную функцию (в которой, например, сотня операторов) встраиваемой, то компилятор сочтёт её слишком большой, и будет её использовать как обычную.
Библиотечные функции
Практически с первого урока мы уже использовали функции:
_getch();
setlocale(LC_CTYPE,"Russian");
Эти функции называются библиотечными. Их объявления находятся в заголовочных файлах, которые мы включаем в наши исходники с помощью #include. Определения этих функций расположены в библиотечных файлах с расширением .lib. Заголовочные файлы расположены в папке studio\VC\include, а библиотечные в studio\VC\lib (studio - папка, куда вы установили Visual Studio C++ 2008). Посмотрите на содержимое заголовочных файлов: conio.h и iostream.
Программа pseudo_game_0_3
Прежде чем продолжить, откройте в IDE листинг программы
Для программы мы напишем две функции: game_init будет инициализировать массив map, а movement_if будет отвечать за передвижение игрока (if ознчает, что в функции будет применяться ветвление if).
Объявление функций
Сразу после директивы using namespace добавьте определение двух констант: s (от strings - строки), c (от columns - столбцы) - это количество строк и столбцов в массиве. Инициализируйте их значениями 15 и 20. Перед определением main нужно добавить объявления пользовательских функций game_init, movement_if:
using namespace std; const int s = 15; const int c = 20; int game_init(char map[s][c]); int movement_if(int&, int&,char map[s][c]); int main()
В game_init() передаётся массив map, а в movement_if - два аргумента по ссылке и массив.
Определение функций
void game_init(char map[s][c]) { for (int i = 0; i < s; i++) { for (int j = 0; j < c; j++) { map[i][j] = ' '; } } map[0][0] = 'Т'; return 0; }
В данную функцию мы вынесли инициализацию массива. Обратите внимание, что счётчики сравниваются с константами s и c. Вместо s и c, мы могли бы использовать конкретные числа, но с помощью данных переменных можно в любой момент времени изменить размер игрового поля, изменив только два числа.
Теперь рассмотрим функцию movement_if.
Переменные ch и act мы поместим в новую функцию, они больше нигде не используются. Также в функции будет подсчитываться сколько ходов сделал игрок.
int movement_if (int& x, int& y, char map[s][c]) { char act; int ch; static int counter = 0; int e = 0; act = _getch(); ch = static_cast(act); if (ch == -32) {} else if (ch == 27) e = 1; // мы больше не можем использовать break else { // здесь мы можем закодировать ещё какое-нибудь значение e } return e; }
Здесь я убрал код проверки нажатия стрелочек. Для краткости.
Функция возвращает значение целого типа, а именно переменную e. Раньше для выхода из программы мы использовали переменную break. Сейчас мы не можем её использовать, так как произойдёт всего-лишь выход из функции movement_if. Вместо этого мы используем переменную e (от error - ошибка), в которую мы сохраняем коды ошибок. 0 - всё в порядке, продолжается выполение программы. 1 - нажата клавиша Esc. Мы можем продолжить, заполнив блок else: 2 - нажата неверная клавиша и закодировав какое-либо действие. Возврашаемое значение присваивается переменной с таким же именем. Кстати, имена этих переменных могли бы быть разными т.к. они обе локальные и никак не связаны друг с другом.
Ну и обратите внимание на параметры функции. Мы используем ссылки на x,y для хранения координат и массив.
Статическая переменная counter подсчитывает количество ходов. Добавьте инкремент переменной counter в каждый блок, где проверяется нажатие стрелочек.
Вызов функции movement_if() осуществляется очень просто:
e = movement_if(x,y,map); if (e == 1) break;
Переменной e присваивается возвращаемое функцией значение, затем проверяется код ошибки. Если 1, значит была нажата клавиша Esc и нужно закончить выполнение программы.
Заключение
Урок по функциям получился очень большим, вам понадобится время чтобы всё понять и осмыслить.
Самое важное, что нужно усвоить в самом начале: функция - это всего-лишь группа операторов у которой есть имя.
Функции используются в двух основных случаях:
1. Если операторы, составляющие тело функции, встречаются в программе много раз. Тогда имеет смысл создать для них отдельную функцию. При этом произойдёт сокращение кода.
2. Для организации кода, если несколько операторов программы выполняют какую-то конкретную задачу. При этом размер кода может и не уменьшится, но программа станет более читабельной.
Посмотрим вот на такой пример:
void simple_function () { оператор 1; оператор 2; оператор 3; оператор 4; } int main () { simple_function(); // вызов функции. // другие операторы simple_function(); // вызов функции. return 0; }
Без simple_function программа будет выглядеть вот так:
int main () { оператор 1; оператор 2; оператор 3; оператор 4; // другие операторы оператор 1; оператор 2; оператор 3; оператор 4; return 0; }
Этот код - это так сказать сущность функций. :)
Всё остальное: возвращаемое значение, список аргументов, передача по ссылке, оператор return, статические переменные - это дополнительные возможности. Да, они чрезвычайно важны, но всё-таки, это дополнительные возможности.
комментарии отсутствуют
авторизуйтесь