Найти в Дзене

Указатели на функции в C++ - от котов к стратегиям

Представь, что ты шеф-повар. У тебя есть поваренная книга, и вместо того чтобы переписывать рецепт «Пасты Карбонара» каждый раз, когда приходит гость, ты просто кладешь в книгу закладку с номером страницы. Указатели на функции — это и есть такие закладки в коде. Они хранят не сами данные, а адрес действия, которое нужно выполнить. Звучит страшновато? На самом деле это просто суперсила, которая делает твой код гибким, как йог. Погнали разбираться! 1. Самые простые: Закладки на рецепты Допустим, у тебя есть простая функция — «позвать кота по имени»: #include <iostream> void callCatByName(int fishCount) { std::cout << "Кис-кис! Я принес " << fishCount << " рыбок!\n"; } Чтобы сделать на неё закладку (указатель), нужно сделать три простых шага: 1. Объявить закладку bookmark: Пишем void (*callCat)(int); * void — потому что функция ничего не возвращает. * (*callCat) — это название нашей закладки (обязательно со звездочкой в скобках!). * (int) — функция ждет на вход число (количество

Представь, что ты шеф-повар. У тебя есть поваренная книга, и вместо того чтобы переписывать рецепт «Пасты Карбонара» каждый раз, когда приходит гость, ты просто кладешь в книгу закладку с номером страницы.

Указатели на функции — это и есть такие закладки в коде. Они хранят не сами данные, а адрес действия, которое нужно выполнить.

Звучит страшновато? На самом деле это просто суперсила, которая делает твой код гибким, как йог. Погнали разбираться!

1. Самые простые: Закладки на рецепты

Допустим, у тебя есть простая функция — «позвать кота по имени»:

#include <iostream>

void callCatByName(int fishCount) {

std::cout << "Кис-кис! Я принес " << fishCount << " рыбок!\n";

}

Чтобы сделать на неё закладку (указатель), нужно сделать три простых шага:

1. Объявить закладку bookmark: Пишем void (*callCat)(int);

* void — потому что функция ничего не возвращает.

* (*callCat) — это название нашей закладки (обязательно со звездочкой в скобках!).

* (int) — функция ждет на вход число (количество рыбок).

2. Положить закладку в книгу: callCat = callCatByName; (можно написать и &callCatByName, но C++ сам понимает).

3. Воспользоваться закладкой: callCat(5);

В коде это выглядит так:

void (*callCat)(int); // Шаг 1: Создали пустую закладку

callCat = callCatByName; // Шаг 2: Воткнули ее на страницу с функцией

callCat(3); // Шаг 3: О, сработало! Кот прибежал за 3 рыбками.

Важно: Закладка подходит только к тем рецептам, у которых точно такое же описание (сигнатура). Нельзя закладкой для «позвать кота (с рыбой)» пользоваться для рецепта «сварить суп (с водой)».

2. Даем клички: typedef и using

Каждый раз писать void (*callCat)(int) — это как говорить «Пожалуйста, передайте мне вон тот кусок бумаги прямоугольной формы с зазубринами». Долго и нудно.

Гораздо проще дать этому типу указателя короткое имя. Это как позвать друга по кличке «Рыжий» вместо «Мой друг с рыжими волосами».

// Старый дедовский способ

typedef void (*CatCaller)(int);

// Новый, модный способ (C++11)

using CatCaller = void(*)(int);

А теперь магия:

CatCaller mySuperPointer = callCatByName; // Коротко и ясно

mySuperPointer(10); // Кот обжирается рыбой

3. Универсальный швейцарский нож: std::function

Это, пожалуй, самое крутое изобретение. std::function — это как коробка для лего, в которую можно засунуть любую деталь: обычную функцию, лямбду (о них позже) или метод класса.

Подключаем библиотеку и пишем:

#include <functional>

#include <iostream>

void catVoice() {

std::cout << "Мяу! (обычная функция)\n";

}

int main() {

// Объявляем контейнер, который хранит "вызывалки", которые ничего не принимают и не возвращают

std::function<void()> soundMaker;

soundMaker = catVoice; // Кладем обычную функцию

soundMaker(); // Вызов: Мяу!

soundMaker = []() { // Кладем лямбда-функцию (анонимного помощника)

std::cout << "Муррр! (лямбда)\n";

};

soundMaker(); // Вызов: Муррр!

return 0;

}

Зачем это нужно? Представь, что ты пишешь игру и тебе нужно передать звук выстрела в функцию «выстрелить». С std::function тебе плевать, откуда этот звук взялся — из библиотеки, из кода игрока или это просто пиу-пиу.

4. Указатели на методы классов (или как заставить объект работать)

Тут есть небольшой подвох. Если функция живет внутри класса, просто так её не вызвать — нужен сам объект (или ключевое слово static).

Статические методы (проще)

Это как общая доска объявлений. Вызываем без объекта.

class Cat {

public:

static void sayStatic() {

std::cout << "Мяу (статический)\n";

}

};

// Использование

void (*staticFunc)() = Cat::sayStatic; // Просто, как с обычной функцией

staticFunc();

Нестатические методы (интереснее)

Тут метод принадлежит конкретному коту. Чтобы он сказал «Мяу», нужен сам кот Васька.

class Cat {

public:

void say() {

std::cout << "Мяу! Я Васька!\n";

}

};

int main() {

// Объявляем указатель на метод класса Cat

void (Cat::*catSayPtr)() = &Cat::say;

Cat myCat; // Создаем кота

(myCat.*catSayPtr)(); // Вызов: "Мяу! Я Васька!" (синтаксис страшноват, но привыкнуть можно)

return 0;

}

5. Лямбды: Функции-ниндзя

Лямбды — это временные функции, которые можно создавать на лету. Они невероятно удобны для мелких задач. Компилятор сам догадается, какого они типа (используй auto).

auto purr = [](int volume) {

std::cout << "Мурлыкаю с громкостью " << volume << "\n";

};

purr(11); // Мурлыкаю с громкостью 11

Где это реально применяется? (Чтобы не заснуть от теории)

1. Кнопки в игре или приложении

Ты нажимаешь кнопку «Атаковать», а программа смотрит: «Ага, у меня в указателе записана функция berserkRage(), вот её и вызову». Если захочешь изменить действие кнопки, просто подставишь другой указатель.

using Action = std::function<void()>;

Action buttonAction;

buttonAction = []() { std::cout << "Вжух! Огненный шар!\n"; };

// buttonAction = openInventory; // Можно и открыть инвентарь, если поменять указатель

buttonAction(); // Вжух!

2. Сортировка чего угодно

Функция std::sort умная, но она не знает, как сортировать твоих котов — по длине хвоста или по наглости морды. Ты просто передаешь ей указатель на свою функцию сравнения.

#include <vector>

#include <algorithm>

#include <iostream>

struct Cat {

std::string name;

int impudenceLevel; // Уровень наглости

};

bool compareByImpudence(const Cat& a, const Cat& b) {

return a.impudenceLevel > b.impudenceLevel; // Самые наглые вперед

}

int main() {

std::vector<Cat> cats = {{"Барсик", 5}, {"Мурзик", 10}, {"Рыжик", 1}};

// Передаем указатель на функцию как критерий сортировки

std::sort(cats.begin(), cats.end(), compareByImpudence);

// Теперь Мурзик (10 наглости) будет первым!

return 0;

}

3. Паттерн «Стратегия» (смена поведения на лету)

Ты пишешь навигатор. Сначала он прокладывает маршрут на машине (calcCarRoute). Потом пользователь выбирает велосипед — и ты просто подменяешь указатель на функцию на calcBikeRoute. Код не ломается, всё работает.

using RouteStrategy = std::function<void()>;

RouteStrategy myRoute;

myRoute = []() { std::cout << "Едем по шоссе, пробки учтены.\n"; };

myRoute(); // Маршрут для авто

myRoute = []() { std::cout << "Едем через парк, короткий путь.\n"; };

myRoute(); // Маршрут для вело

Итог: Стоит ли оно того?

Поначалу эти звездочки и амперсанды в объявлениях кажутся китайской грамотой. Но как только ты поймешь суть (мы просто храним адрес действия), весь этот синтаксис перестанет пугать.

Главное, что нужно вынести

- Указатели на функции позволяют хранить поведение в переменных.

- Они делают код гибким (можно подставить нужную функцию по ходу дела).

- Современный C++ с std::function и лямбдами делает работу с ними простой и даже увлекательной.