Пройдя не такой уж долгий путь от нашего первого урока к этому, вы “подошли” к изучению функций в C++. Функции – это именованный фрагмент кода, который повторяется в программе 2 или больше раз . Когда мы пишем функцию, необходимо дать ей имя и в дальнейшем, чтобы её вызвать в программе (из main() или из другой функции), надо обратиться к ней по этому имени.
Мы уже встречали функции в предыдущих уроках. Это функции для строк (символьных массивов) strlen(), strcmp(), функция для генерации случайных чисел rand(). Мы их применяли в программах и, например, передавали в функцию strlen() строку, а она нам возвращала количество символов в этой строке (целое число).
Это конечно происходило не волшебным образом, а функция принимала нашу строку, обрабатывала её и возвращала нам значение, которое подсчитала. То есть кто-то до нас написал этот самый код функции, которая считает длину строки и мы успешно ею пользуемся в своих программах. И эта функция здорово экономит наше время, сокращает количество строк кода и облегчает его читаемость.
Да – есть эти замечательные стандартные библиотечные функции, которые мы можем применять в своих программах, но в большинстве случаев каждое новое задание уникально и стандартные функции не всегда подойдут. В С++ программист может самостоятельно написать собственную функцию и применять её с таким же успехом, как и библиотечные функции.
До определённого времени можно обходиться и без функций. Вместо этого плодить одинаковый участок кода во всей программе. Но если придется изменить этот код (усовершенствовать или что-то убрать из него), придется вносить изменения по всей программе. Лучше сразу освоить тему функций и активно применять.
Определить функцию можно двумя способами:
- до main-функции;
- после main-функции. В этом случае необходимо до main-функции объявить прототип собственной функции.
В этой статье и следующих мы будем пользоваться вторым способом, так как он является более распространённым. Первый способ можно использовать, если функция одна и её код совсем небольшой. Пока мы пишем простые программы, такое встречается часто. Но для программ посложней, будем писать несколько функций которые будут состоять не из 2-3 строк, а побольше. Покажу вам как выглядит определение функции до main():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #include <iostream> using namespace std; void ourFunctionForPrint() // определение функции до main() { cout << "Сайт purecodecpp.com\n"; cout << "Изучаем функции в C++\n"; cout << "Эта функция выводит на экран три строки\n\n"; } int main() { setlocale(LC_ALL, "rus"); cout << "Вызов ourFunctionForPrint() из main()\n\n"; ourFunctionForPrint(); cout << "Функция ourFunctionForPrint() отработала!\n"; cout << "Далее продолжает работу main()\n\n"; return 0; } |
С использованием прототипа это будет выглядеть так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include <iostream> using namespace std; void ourFunctionForPrint(); // прототип int main() { setlocale(LC_ALL, "rus"); cout << "Вызов ourFunctionForPrint() из main()\n\n"; ourFunctionForPrint(); cout << "Функция ourFunctionForPrint() отработала!\n"; cout << "Далее продолжает работу main()\n\n"; return 0; } void ourFunctionForPrint() // определение { cout << "Сайт purecodecpp.com\n"; cout << "Изучаем функции в C++\n"; cout << "Эта функция выводит на экран три строки\n\n"; } |
Прототип функции размещен в строке 4, а её определение находится в самом низу программы в строках 20 – 25. Что касается выполнения программы: сначала компилятор прочтет прототип. Это даст ему знать о том, что где-то после main() располагается определение этой функции.
Далее начнется выполнение главной функции main(). Выполняться она будет, пока компилятор не встретит имя функции ourFunctionForPrint(). Тогда он найдет определение этой функции, которое расположено после main(), по имени, указанному в прототипе, выполнит её код, после чего снова вернется к выполнению команд main-функции.
В итоге на экране увидим:
Поговорим об определении функций.
Функции в C++ могут не возвращать никаких значений (как в примере) и могут возвращать какое-либо значение. Если функция не возвращает ничего, то это функция типа void.
Синтаксис функции, которая не возвращает значений:
Имя функции следует давать придерживаясь правил для имен переменных. Единственное – желательно чтобы оно содержало глагол, так как функция выполняет действие. Например если она считает среднее арифметическое можно дать название calculateAverage, если выводит что-то на экран – showText. Имя должно говорить за себя, чтобы не пришлось оставлять лишние комментарии в коде.
Параметры (или аргументы функции) – это данные, которые функция принимает и обрабатывает в теле. Если функции не нужно ничего принимать для обработки, круглые скобки оставляют пустыми. Согласно правилам High Integrity C++ Coding Standard желательно не определять функции с большим количеством параметров (больше 6).
Рассмотрим пару примеров с функциями, которые принимают параметры и не возвращают значений.
Принимает один параметр:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #include <iostream> using namespace std; void printQuestion(int questionCount); int main() { setlocale(LC_ALL, "rus"); printQuestion(7); cout << endl << endl; return 0; } void printQuestion(int questionCount) { for (int i = 0; i < questionCount; i++) { cout << '?'; } } |
В 10-й строке кода функция получает параметр – целое число 7. С ним (с этим числом) произойдет то, что описано в определении функции – строки 16 – 22. А именно – это число подставится в заголовок цикла for. Выражение i < questionCount станет равнозначным i < 7 . В итоге мы увидим на экране 7 знаков вопроса.
Принимает три параметра:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | #include <iostream> using namespace std; void printOurSymbol(int symbCount, int lineCount, char ourSymbol); int main() { setlocale(LC_ALL, "rus"); printOurSymbol(7, 5, '@'); cout << endl << endl; return 0; } void printOurSymbol(int symbCount, int lineCount, char ourSymbol) { for (int i = 0; i < lineCount; i++) { for (int j = 0; j < symbCount; j++) { cout << ourSymbol; } cout << endl; } } |
Синтаксис функции, которая возвращает значение:
Такие функции отличаются тем, что необходимо указать тип значения, которое вернет функция в результате своей работы. Сам возврат значения в программу оформляется оператором return и это значение программа получит в том месте, где функция была вызвана . return может возвращать переменную, константу или результат выражения (например: return variable1 - variable2; ). В теле функции могут находиться несколько операторов return. Тогда, работа функции завершится, если сработает какой-то один из этих операторов. Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | #include <iostream> using namespace std; int calculateSomeDigits(int d1, int d2, char ch); int main() { setlocale(LC_ALL, "rus"); int digit1 = 0; int digit2 = 0; int res = 0; char chVar = 0; cout << "1-е число: "; cin >> digit1; cout << "2-е число: "; cin >> digit2; cout << "Операция(+ или -): "; cin >> chVar; res = calculateSomeDigits(digit1, digit2, chVar); cout << "res = " << res << endl; return 0; } int calculateSomeDigits(int d1, int d2, char ch) { if (ch == '+') return d1 + d2; if (ch == '-') return d1 - d2; } |
Определение функции располагается в строках 28 – 34. Если пользователь введет +, сработает блок if в строке 30, а в нём соответственно сработает return d1 + d2; . После этого код функции уже не будет обрабатываться дальше. Компилятор вернется к выполнению main-функции.
Вы наверное заметили, что в предыдущем коде названия параметров в прототипе и в самом определении функции отличаются от имен переменных, которые передаются в функцию из main. Дело в следующем – параметры в определении и прототипе функции формальные. Когда мы передаем переменные в виде параметров, функция будет работать не с оригиналами переменных, а с их точными копиями. Эти копии создаются в оперативной памяти в момент вызова функции. Она работает с этими копиями, а по завершении работы, копии уничтожаются. Так что в прототипах вы можете использовать точные имена переменных, но функция в любом случае будет работать не напрямую с ними, а с их копиями. То есть сами переменные она не может изменить. Когда в следующих уроках вы познакомитесь с указателями и ссылками – узнаете, как можно изменить значения передаваемых переменных в теле функции.
Еще немного о прототипе: прочитав его до main, компилятор получает сведения о том, какой тип возвращаемого значения будет у функции (или она вообще не возвращает значения – имеет тип void) и о том, какие параметры будут в неё переданы, в каком количестве и в какой последовательности.
Прототип int calculateSomeDigits(int d1, int d2, char ch); говорит компилятору, что функция вернет на место её вызова целое число и о том, что при вызове в нее должно быть передано два целых числа и один символ. При вызове функции, мы должны передать ей столько параметров сколько указано в её заголовке при определении.
Передавать параметры необходимо в том же порядке, как они определены в круглых скобках за именем функции. Иначе возникнут ошибки при компиляции либо программа будет работать некорректно.
Синтаксис прототипа функции:
Если параметров несколько – они должны отделяться запятой. Легче всего объявить прототип – это скопировать из определения функции первую строку (заголовок) и после закрывающей круглой скобки добавить точку с запятой.
Имена переменных-параметров в прототипе можно не указывать. Следующий прототип равнозначен тому, что выше.
На мой взгляд, все же лучше объявлять прототипы функций с указанием имён параметров. Особенно если параметров несколько и они имеют одинаковый тип. Для читаемости и понимания программы так будет лучше.
Чтобы закрепить то, о чём говорили в этой статье, надо попрактиковаться. Смотрите статью с задачами на функции в C++ . В ней вы так же найдете информацию о том, как передавать в функции массивы в виде параметров. Совет – не просто читайте, а пишите код! Желательно своими силами.
Видео по теме:
>> Функции – это именованный фрагмента кода, который повторяется в программе 2 или больше раза .
Функция – это именованный фрагмент кода. По имени (он же именованный) этот фрагмент может вызываться сколько угодно раз (даже ноль раз). В старых книжках писали, что если в программе есть фрагмент кода, который повторяется 2 и более раз, то он претендует на то, чтобы стать функцией.
Вот так я бы примерно раскрыл эту мысль.
>> Когда мы пишем функцию, необходимо дать ей имя и в дальнейшем, чтобы её вызвать в main-функции, надо обратиться к ней по этому имени.
Проще. Функции должно быть присвоено имя для того, чтобы мы могли к ней обратиться (по имени). Обратиться можно откуда угодно, а не только из main().
Например:
int foo() { return 1; }
int bar() { return foo(); } // типа обращаемся к функции не из main
int main() {
cout < < bar(); }
Определить функцию можно двумя способами: до main-функции; после main-функции.
Функция может быть определена где угодно, даже в другом файле (не в main.cpp, а скажем, в foo.cpp).
Определение функции включает в себя заголовок (это тот кусок, где мы указываем возвращаемое значение, имя функции и типы аргументов) и тело (это собственно код, который выполняет функция).
Но мы можем объявить функцию, а тело ей не написать. Так делают чтобы указать компилятору что такая функция где-то есть и он не сыпал нам ошибками. Если в конце концов функции не окажется (при сборке программы), то ошибку нам выдаст линкер (он занимается этим делом).
Объявление функции содержит лишь заголовок.
Короче, если функция foo() вызывает функцию bar(), то функция bar() должна быть либо объявлена, либо определена по коду выше чем foo().
Например
int foo() { return 1; }
int bar() { return foo(); }
Сработает без ошибок.
int bar() { return foo(); }
int foo() { return 1; }
Вы получите сообщение компилятора о том, что функции foo() нету.
int foo(); // это объявление без реализации
int bar() { return foo(); }
int foo() { return 1; } // этот код может быть где угодно (до bar(), после bar() или даже в другом файле
Сработает без ошибок.
>> Если функции не нужно ничего принимать для обработки, круглые скобки оставляют пустыми. Согласно правилам High Integrity C++ Coding Standard желательно не определять функции с большим количеством параметров(больше 6).
Вот это правильно. Можно встретить товарищей, которые при отсутствии параметров пишут void в круглых скобках, так:
int foo(void) { return 1; }
Это вроде бы какой-то атавизм от древних реализаций Си-компиляторов. Так делать не надо.
Про большое количество параметров тоже верно, я не читал High Integrity C++ Coding Standard (буду теперь знать что там про 6 аргументов написано), но читал весьма популярную в узких кругах книжку Мартина “чистый код”. Там он пишет, что чем меньше параметров, тем лучше. В идеале – нет параметров, но предельным он вроде бы называет количество параметров, равное трем. Мартин мотивирует это эпически высокой сложностью тестирования функций с большим количеством параметров.
Я просто чуть дополнил Ваш тезис :).
>> Вы наверное заметили, что в предыдущем коде названия параметров в прототипе и в самом определении функции отличаются от имен переменных, которые передаются в функцию из main.
Ага, они могут отличаться, но лучше если они совпадают. В прототипе функции имена вообще можно не писать, так:
int foo(int, float);
// …
int foo(int a, float b) { return a + b; }
Но лучше их все таки писать, т.к. среда разработки (ваша Visual Studio) подскажет вам тип и имя параметров, когда вы наберете имя функции и откроете скобочку. Информацию IDE берет из прототипа. Если вы дадите параметрам хорошие имена – то вижуал студия сможет вам более качественно помогать :)
полностью поддерживаю – лучше, чтобы имена переменных и имена параметров в заголовке функции совпадали. Для себя мы знаем, что на самом деле это разные области памяти, а при вызове функции – действительно реальная помощь.
С тем что, лучше прописывать имена параметров в прототипе тоже соглашаюсь.
В прототипе областей памяти вообще нет. Вы же написали, что память выделяется в начале работы функции и освобождается в конце. А прототип – это просто пометка для компилятора, чтобы он не ругался…
Всё верно. О разных областях памяти – это было о переменных и о параметрах в заголовке функции.
всё боялась я приступать к этой теме. Но прочитав Вашу статью о функциях поняла, что не всё так страшно, как казалось! Спасибо админу за труд! Я ваш постоянный читатель теперь :)
Кстати в книге “Чистый код” Мартина целый раздел посвящен функциям. Я думаю весьма интересно.
Например, если верить Мартину, то функция должна выполнять ровно одну задачу, а имя функции эту задачу четко описывать. Отсюда вытекает то, что размер функций должен быть очень маленьким (в старых книжках писали что функция должна умещаться в 80 строк {на старых мониторах это один экран}, но реально, я считаю что чаще всего функцию можно и нужно сделать короче).
Ну и все эти “если фрагмент кода повторяется несколько раз…” тоже становятся не совсем актуальными при таком подходе. Просто, если фрагмент кода выполняет какую-то осмысленную операцию, то логично дать этой операции имя, завернув код в функцию.
У функций есть небольшой оверхед связанный с вызовом функции (и как тут правильно писали, копированием параметров при передаче их по значению {как делают во всех примерах этой статьи}), но при правильном подходе большую часть функций можно объявить как constexpr и inline, а параметры передавать по ссылке (я думаю, желающие поднимут литературу :) ).
Programmer_blog, спасибо, что дополнили мою статью интересной актуальной информацией.
Вам уже колонку надо выделить на нашем сайте, для комментирования статей )