Запаковываем наш алгоритм в функцию. Как её создать? Что такое декларация и определение, и почему важно различать их?
Зачем нам функции?
В прошлой части мы написали небольшую программу FizzBuzz, весь алгоритм которой был заключен в int main. Для тривиальных задач это нормально, но с ростом сложности такой подход становится неприемлемо неудобным: увеличивается размер файла, время компиляции, и самое главное — нет повторного использования нашего кода.
Создав функцию, мы cможем повторно использовать наш код, а также разделить его на разные файлы (в следующей статье).
Немножко типов
Пока тебе стоит знать только три типа переменных:
- int — целочисленная переменная, может принимать и положительные и отрицательные значения.
- float — переменная с точкой, принимает практически любые числа но имеет некоторые нюансы, проблемы с точностью и требует аккуратного обращения.
- void — неполноценный тип. Переменную такого типа создать нельзя. Указывает на отсутствие типа.
Определение функции в Си
Новая функция создается так:
Тип-результата имя-функции(список-аргументов-или-void){
код-функции
};
В этом выражении:
- тип-результата — тип переменной что возвращает функция. Может быть void, если функция ничего не возвращает
- имя-функции — уникальное имя функции. В Си невозможно создать функции с одинаковыми именами, но с разным кодом или типами
- список-аргументов-или-void — либо void (если функция ничего не принимает), либо непустой список аргументов (1 или более) через запятую
- аргумент — одно или два слова: тип-аргумента + не обязательное имя-аргумента через пробел. Тип не может быть void.
- код-функции — описание алгоритма функции. Если тип-результата не void, обязан содержать return (об этом далее)
А что на практике? Собираем всё в кучу
Для того чтобы освоиться с синтаксисом, создадим несколько бесполезных функций ( с пустым телом {} )
void useless(void){};
int main(){
useless();
}
Функция не принимает и не возвращает аргументы
void useless(int, int){};
int main(){
useless(1,2);
}
Функция принимает два числа типа int,
но не может с ними взаимодействовать — у них нет имени
void useless(int myvar){};
int main(){
useless(17);
}
Функция принимает одно число. Может использовать его в своей логике
int getOne(void){
return(1);
};
int main(){
getOne();
}
Функция ничего не принимает. Возвращает всегда 1. Об этом далее
Все эти функции можно достаточно легко расширить, если добавить код в пустое тело.
Возвращение значений
Для того чтобы окончить исполнение функции, используется ключевое слово return
Есть очень важное правило, что многие новички упускают: если тип результата функции не void и функция не main, то она обязана иметь return, иначе это
Пример простой функции:
int my_formula(int x, int y){
return(x*2 + y + 1);
};
Скобки вокруг return не обязательны и обычно не используются. Return — это ключевое слово языка, а не функция:
int my_formula(int x, int y){
return x*2 + y + 1;
};
Преждевременное завершение функции
Return можно использовать и просто для того чтобы избегать неприятные входные данные, или завершить функцию преждевременно. Например, если функция ожидает не нулевое значение:
void divide(int x){
if( x == 0) {
return;
}
// Код
// Можно рассчитывать что X не 0
};
Также, можно выходить из цикла while:
void loop(int x) {
while (true) { // Бесконечный цикл
// Код
if (true) { // Условие выхода
return;
}
// Код
}
};
Это весьма экзотический способ выйти из цикла, но в таком варианте выхода могут быть преимущества. Возможно исполнение кода до проверки условия. А также выход происходит из всей функции, а не из цикла — в некоторых случаях это может быть особенно полезно.
Практический пример
Напишем небольшой код для демонстрации:
Создать код что выводит для всех чисел от 1 до 100 запись: «X is prime» или «X is not prime», в зависимости от того — является ли число простым
Также вспомним определение простого числа:
Положительное число X является простым только в том случае, если оно делится нацело только на 1 и на само себя. Исключение — число 1 не простое.
Для начала, напишем всё в одной функции. Для определения является ли число простым, будем использовать тривиальный алгоритм — просто перебирать все делители от 2 до X-1. Это далеко не самый эффективный способ, но это сейчас не важно.

int printf(const char * , ...);
int main() {
// Начальное значение
int X = 1;
// Цикл с проверкой того что
// всё ещё нужно считать
while (X <= 100) {
int count_divs = 0; // Количество делителей
int divisitor = 2; // Перебираем делители начиная с 2
while (divisitor <= X - 1) { // Перебираем до X-1
// Проверим, делится ли число нацело
if ((X % divisitor) == 0) {
// Если да, увеличим число делителей
count_divs = count_divs + 1;
}
divisitor = divisitor + 1; // Следующий делитель
}
printf("%d" " ", X); // Печатаем
// Убрали \n чтобы не было перевода на новую строку
// Если нет делителей и X не 1
if ((count_divs == 0) && (X != 1)) {
printf("is prime" "\n");
} else {
printf("is not prime" "\n");
}
X = X + 1; // Следующее число
}
};
Код получился достаточно уродливый, но я настоятельно рекомендую изучить его, поскольку он содержит достаточно много базового функционала. Я же перехожу к обсуждению того, почему он такой уродливый и как это исправить:
Главная причина уродства заключается в том, что мы пытаемся решить несколько проблем за раз.
Здесь нет никаких абстракций, множество переменных взаимодействуют друг с другом и не ясно к чему это может привести. Работать с таким кодом сложно и неприятно.
Изолируем функционал
Решим эту проблему создав отдельную функцию что проверяет — целое число или нет. Если число целое, возвращает 1, иначе 0:
int is_prime(int X) {
// Избегаем 1 и неположительные числа
if (X < 2)
return 0;
int divisitor = 2;
while (divisitor < X - 1) {
if ((X % divisitor) == 0) {
// Нашли делитель - сразу возвращаем 0
return 0;
}
divisitor = divisitor + 1;
}
// Не нашли делителей - число простое
return 1;
}
Здесь множество преимуществ, но обсудим их после. Пока напишем и новую функцию main что использует is_prime»
int main() {
int X = 1;
while (X <= 100) {
printf("%d" " ", X);
if (is_prime(X)) {
printf("is prime" "\n");
} else {
printf("is not prime" "\n");
}
X = X + 1;
}
};
Мы выделили функционал отдельно, и тем самым получили множество преимуществ:
- Повышение читаемости — разработчик сможет увидеть что делает функция main не особо вникая. Хорошее название функции is_prime тому способствует.
- Изоляция функционала — теперь код is_prime находится отдельно, это облегчит его изменения/оптимизации, если в один момент это понадобится.
- Возможность повторного использования — не придётся копировать огромный текст, если где-то в другом месте придется ещё раз проверить число на простоту.
- Облегчение использования — стало гораздо легче поддерживать код и вносить изменения.
Для демонстрации — изменим задачу и посмотрим на то как изменится main. Новая задача:
Среди диапазона чисел от 1 до 100 включительно найти все пары простых чисел что отличаются на 2.
Это не составит особого труда после нашего рефакторинга:

int printf(const char * , ...);
// Старый код функции is_prime
int is_prime(int X) {
if (X < 2)
return 0;
int divisitor = 2;
while (divisitor <= X - 1) {
if ((X % divisitor) == 0) {
return 0;
}
divisitor = divisitor + 1;
}
return 1;
}
int main() {
int X = 1;
// Сменили предел
while (X <= 98) {
// Теперь проверяем что нашли пару
if (is_prime(X) && is_prime(X + 2)) {
// Печатаем
printf("Found:" " ");
printf("%d" " ", X);
printf("%d" "\n", X + 2);
}
X = X + 1;
}
};
Изменения кода минимальны. Думаю на этом стоит заканчивать, но сначала…
Хорошие свойства функций
Небольшой список качеств что делают функции лучше:
- Детерминированность — результат выполнения функции зависит только от аргументов и всегда одинаковый для одного набора аргументов.
- Отсутствие побочных эффектов — функция не делает ничего «грязного» — не печатает в консоль/файлы. Не меняет глобальные переменные.
- Чистая функция — Одновременно пункты 1 и 2
- Реентерабельность — Более сложное понятие связанное с тем что функцию можно одновременно вызывать из разных потоков. Намекает на отсутствие скрытых состояний внутри функций. Об этом в другой статье.
В нашем коде функция is_prime обладает всеми этими качествами, это и делает её такой полезной.
Опасность: абстракции не панацея
Хорошие свойства и абстракции ещё не гарантируют хороший код. Зачастую всё зависит от конкретной ситуации и избыточный фанатизм зачастую только вредит.
Далее — извлекаем is_prime в другой файл
В следующей статье расскажу про декларацию функций, как это помогает разделять проект на раздельные файлы и немного про то как это всё собирается в крупных проектах.