Функции


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

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

<тип данных> <имя функции> (<набор параметров>) {
  <тело функции>
}

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

<имя функции>(<набор параметров>);

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

<имя функции>();

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

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


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

byte a, b;
int c;

void setup() {
  a = 10;
  b = 20;
  sumFunction();
  // после вызова функции
  // с имеет значение 30
}

void loop() {
  

}

void sumFunction() {
  c = a + b;
}

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

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

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


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

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

Один момент: функция типа void вроде бы ничего не возвращает, но использование 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, складывает их и возвращает. Это и есть выполнение концепции “отделение кода от данных”, функция живёт сама по себе и не зависит от других переменных!

Казалось бы, можно использовать функцию как 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);
}

А как быть, если мы хотим складывать уже имеющийся функцией другие типы данных? Например 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 – получили их сумму при помощи первой функции. Вот такая удобная штука!

Макро-функции


Вы наверное уже помните такую директиву препроцессору, как #define. Из урока про переменные и константы мы узнали, что при помощи дефайна можно задавать константы. Ключевая особенность работы define заключается в том, что он заменяет последовательность символов чем угодно, что мы там напишем, и это даёт возможность создавать так называемые макро-функции (macro), которые не создаются как функции, а вставляются в код при компиляции. Например вот так будет выглядеть макро, складывающая два числа:

#define sum(x, y) ((x)+(y)) 

На этапе компиляции в данном документе с кодом все встречающиеся sum(значение1, значение2) будут заменены на значение1 + значение2, таким образом это будет сумма

#define sum(x, y) ((x)+(y))

void setup() {
  byte a = 10;
  byte b = 20;
  byte c = sum(a, b);
  // с получила значение 30
  // на этапе компиляции выражение sum(a, b)
  // превратилось в (a + b)

  int d = sum(500, 900);
  // d получила значение 1400
}

void loop() {
}

В “языке” Arduino есть несколько готовых инструментов, которые кажутся функциями, но на самом деле являются макро. Заглянем в Arduino.h и увидим следующее:

#define min(a,b) ((a)<(b)?(a):(b))
#define max(a,b) ((a)>(b)?(a):(b))
#define abs(x) ((x)>0?(x):-(x))
#define constrain(amt,low,high) ((amt)<(low)?(low):((amt)>(high)?(high):(amt)))
#define round(x)     ((x)>=0?(long)((x)+0.5):(long)((x)-0.5))
#define radians(deg) ((deg)*DEG_TO_RAD)
#define degrees(rad) ((rad)*RAD_TO_DEG)
#define sq(x) ((x)*(x))

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

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


Иногда бывает нужно передать в функцию массив (мы о них уже говорили), передать именно массив целиком, а не отдельный его элемент. В этом случае уже не обойтись без указателей и ссылок. Урока по ним пока что нет, потому что это сложная тема и я сам пока что не до конца с ней разобрался. Почитать можно здесь. Чтобы передать массив в функцию, совсем не обязательно знать всю матчасть, достаточно просто запомнить суть. В следующем примере наша функция sumFunction будет суммировать элементы массива, который в неё передаётся. Функция заранее знает, сколько в массиве элементов, потому что я явно цифрой указал количество в цикле for.

int c;
int myArray[] = {100, 30, 890, 645, 251};

void setup() {
  c = sumFunction((int*) myArray);   // результат 1916
}

void loop() {


}

int sumFunction(int *intArray) {
  int sum = 0;   // переменная для сложения
  for (byte i = 0; i < 5; i++) {
    sum += intArray[i];
  }
  return sum;
}

Что из этого нужно запомнить: при описании функции параметр массива указывается со звёздочкой, т.е. <тип данных> *<имя массива> . При вызове массив передаётся как (<тип данных>*) <имя массива> . И в целом всё.

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

int c;
int myArray[] = {100, 30, 890, 645, 251, 645, 821, 325};

void setup() {
  // передаём сам массив и его размер в БАЙТАХ
  c = sumFunction((int*)myArray, sizeof(myArray));
}

void loop() {


}

int sumFunction(int *intArray, int arrSize) {
  // переменная для суммирования
  int sum = 0;  

  // находим размер массива, разделив его вес
  // на вес одного элемента (тут у нас int)
  arrSize = arrSize / sizeof(int);  
  for (byte i = 0; i < arrSize; i++) {
    sum += intArray[i];
  }
  return sum;
}

И вот мы получили функцию, которая суммирует массив типа данных int любой длины и возвращает результат!

Видео


Важные страницы


  • Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
  • Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
  • Полная документация по языку Ардуино, все встроенные функции и макро, все доступные типы данных
  • Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
  • Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
Функции
5 (100%) 1 vote[s]

Последнее обновление Май 06, 2019
2019-05-06T13:45:20+03:00