Учебник по "программированию на Arduino"

Лекция №5. Функции

Функция - часть программы, блок кода, имеющий своё название. Большая программа может строиться из нескольких функций, каждая из которых выполняет свою задачу, поэтому можно назвать функцию подпрограммой. Использование функций очень сильно упрощает написание и чтение кода, и в большинстве случаев делает его оптимальным по объёму занимаемой памяти. Функция должна быть описана, и после этого может вызываться. Функция должна быть описана снаружи других функций! В общем виде функция имеет следующую структуру:

тип_данных имя_функции (аргументы) {

тело_функции

}

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

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

имя_функции(аргумент1, аргумент 2, аргументN);

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

имя_функции();

Функция в C++ всегда возвращает результат. Этот термин означает, что после выполнения функции она выдаёт некое значение, которое можно присвоить другой переменной.

результат = имя_функции();

Функция может принимать аргументы, может не принимать, может возвращать какое-то значение, может не возвращать. Давайте рассмотрим эти варианты.

Функция, которая ничего не принимает и ничего не возвращает

Самый простой для понимания вариант, с него и начнём. Помимо типов данных, которые я перечислял в уроке о типах данных, есть ещё один - void, который переводится с английского как "пустота". Создавая функцию типа void мы указываем компилятору, что никаких значений возвращаться не будет (точнее будет - функция вернёт "ничего").

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

byte a, b;

int c;

void setup() {

a = 10;

b = 20;

sumFunction();

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

// с имеет значение 30

}

void loop() {

}

void sumFunction() {

c = a + b;

}

Это очень плохой пример с точки зрения оптимальности кода, но далее мы будем этот пример улучшать и в итоге получим конфетку. Чем он плох на данном этапе: у нас используются глобальные переменные, которые работают внутри функции, а одним из главных принципов программирования является разделение данных и действий. Будучи новичком, не стоит об этом сильно задумываться, позже вы сами к этому придёте. Разделяя данные и действия можно создавать универсальные инструменты, рассмотренная выше функция не является универсальной: она складывает глобальную а с глобальной b и записывает результат в глобальную же c. Сделаем следующий шаг к оптимизации: пусть функция возвращает значение.

Функция, которая ничего не принимает и возвращает результат

Чтобы функция могла вернуть численное значение, она должна быть описана с типом данных, который будет возвращаться. Нужно заранее подумать, какой тип будет возвращён, чтобы избежать ошибок. Например я знаю, что моя суммирующая функция работает с типом данных byte, она складывает два таких числа. Это означает, что результат вполне может превысить предел типа byte (сложили 100 и 200 и получили 300), значит функции следовало бы возвращать например тип данных int. Собственно поэтому у переменной c из предыдущего примера тип данных int.

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

Давайте перепишем наш код так, чтобы числа a и b складывались и результат возвращался функцией, и этот результат мы уже "ручками" приравняем к c.

byte a, b;

int c;

void setup() {

a = 10;

b = 20;

c = sumFunction();

// с имеет значение 30

}

void loop() {

}

int sumFunction() {

return (a + b);

}

Ну вот, функция стала чуть более универсальной. Теперь результат сложения a и b как функцию можно использовать в других местах и приравнивать к другим переменным. Чтобы сделать код ещё более универсальным, давайте передавать величины для сложения в виде аргументов.

Функция, которая принимает аргументы и возвращает результат

Что касается аргументов, то они перечисляются в скобках через запятую с указанием типов данных. При вызове функции указанные аргументы превращаются в локальные переменные, с которыми можно работать внутри функции. Иными словами - это копии переданных переменных! При вызове функции эти переменные получают значения, которые мы указываем при вызове. Смотрим:

byte a, b;

int c;

void setup() {

a = 10;

b = 20;

c = sumFunction(a, b);

// с имеет значение 30

}

void loop() {

}

int sumFunction(byte paramA, byte paramB) {

return (paramA + paramB);

}

И вот так мы получили универсальную функцию sumFunction, которая принимает две величины типа byte, складывает их и возвращает результат типа int. Это и есть выполнение концепции "отделение кода от данных", функция живёт сама по себе и не зависит от других переменных! Казалось бы, можно использовать функцию как sumFunction(100, 200), и она вернёт значение 300. Но не всё так просто, потому что любое целое число в программе обрабатывается компилятором как int, и при попытке передать такой вот int в нашу функцию, которая принимает byte, мы получим ошибку "нельзя передать int вместо byte". В этом случае можно привести тип числа к byte вручную, вот так это будет выглядеть:

int c;

void setup() {

c = sumFunction((byte)100, (byte)200);

// с = 300

}

void loop() {

}

int sumFunction(byte paramA, byte paramB) {

return (paramA + paramB);

}

Но лучше всё-таки сделать функцию более универсальной, пусть она принимает int paramA и int paramB, ведь данные переменные являются локальными, то есть создадутся при вызове функции и удалятся из памяти при завершении работы функции, и их "размер" в принципе не играет роли.

А как быть, если мы хотим складывать уже имеющийся функцией другие типы данных? Например, float. Можно преобразовать типы данных при передаче аргументов, но функция всё равно вернёт целое число. Сделать нашу функцию ещё более универсальной сможет такая штука C++ как перегруженная функция.

Перегруженные функции

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

int c;

float d;

void setup() {

float af = 5.5;

float bf = 0.25;

Serial.begin(9600);

c = sumFunction(10, 20); // результат 30

c = sumFunction(10, 20, 30); // результат 60

d = sumFunction(af, bf); // результат 5.75

Serial.println(c);

Serial.println(d);

}

void loop() {

}

int sumFunction(int paramA, int paramB) {

return (paramA + paramB);

}

int sumFunction(int paramA, int paramB, int paramC) {

return (paramA + paramB + paramC);

}

float sumFunction(float paramA, float paramB) {

return (paramA + paramB);

}

Итак, у нас теперь целых три функции с одинаковым именем, но разными наборами аргументов и типами возвращаемого значения. Программа сама разберётся какую из функций использовать на основе передаваемых аргументов. Передали два float - работает третья функция, вернёт float. Передали три int - получили их сумму при помощи второй по счёту функции. Передали два int - получили их сумму при помощи первой функции. Вот такая удобная штука! Ещё одним вариантом перегруженной функции является шаблонная функция, она позволяет работать с данными любого типа в рамках одной функции, без перегрузов. Читайте ниже.

Функция, которая меняет значение переменных

Забегая немного вперёд (в урок про указатели и ссылки) сразу рассмотрим ещё один полезный вариант: функция меняет значения указанных переменных. В рассмотренных выше примерах мы передавали в функцию переменные по значению. Это означает, что внутри тела функции мы имеем дело с копиями переменных, которые не влияют на изначальные переменные. Если передать переменную по ссылке - внутри функции мы будем работать именно с теми переменными, которые передали в качестве аргументов! Для передачи аргумента по адресу достаточно добавить всего лишь один символ - &. Напишем функцию, которая увеличивает значение переданной переменной на 10:

void setup() {

int a = 10;

incr(a);

// здесь a == 20

}

void incr(int& var) {

var += 10;

}

Таким образом мы снова отделили данные от выполняемого кода, но сделали это ещё более элегантно.

Типы данных

Функция может возвращать и принимать как аргумент любые типы данных в любых сочетаниях: целые числа, float, String, указатели, struct, enum и так далее. Например следующая функция принимает два числа, склеивает их в строку через запятую и возвращает:

String toStr(int val1, float val2) {

return (String)val1 + ", " + val2;

}

При вызове Serial.println(toStr(10, 3.14)) увидим в мониторе порта 10, 3.14

Как вернуть несколько значений?

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

struct MyStruct {

int valSum;

int valSub;

int valMul;

};

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

MyStruct compute(int val1, int val2) {

MyStruct str;

str.valSum = val1 + val2;

str.valSub = val1 - val2;

str.valMul = val1 * val2;

return str;

}

Готово! Пользоваться этим можно так:

void setup() {

Serial.begin(9600);

MyStruct str;

str = compute(3, 4);

Serial.println(str.valSum);

Serial.println(str.valSub);

Serial.println(str.valMul);

}

Выведет в порт 7, -1, 12 Если изучить урок по структурам, то функцию с вычислением и возвратом структуры можно сократить всего до одной строчки кода:

MyStruct compute(int val1, int val2) {

return (MyStruct) {val1 + val2, val1 - val2, val1 * val2};

}

Описание и реализация

Хорошим тоном считается объявлять функции отдельно от реализации. Что это значит: в начале документа, или в отдельном файле, мы описываем функцию (это будет называться прототип функции), а в другом месте пишем реализацию. Так делают в серьёзных программных проектах, Ардуино-библиотеки - не исключение. Также такое написание позволяет слегка ускорить компиляцию кода, потому что компилятор уже знает, что он в нём найдёт. Алгоритм такой:

Описание (прототип) функции:

тип_данных имя_функции(набор_аргументов);

Реализация функции:

тип_данных имя_функции(набор_аргументов) {

тело функции

}

То бишь всё то же самое, но с телом функции и фигурными скобками. Пример:

// описание функций

void lolkek(byte cheburek);

int getMemes();

void setup() {

}

void loop() {

}

// реализация функций

int getMemes() {

// действия

}

void lolkek(byte cheburek) {

// действия

}

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

Web hosting by Somee.com