Представь, что ты шеф-повар. У тебя есть поваренная книга, и вместо того чтобы переписывать рецепт «Пасты Карбонара» каждый раз, когда приходит гость, ты просто кладешь в книгу закладку с номером страницы.
Указатели на функции — это и есть такие закладки в коде. Они хранят не сами данные, а адрес действия, которое нужно выполнить.
Звучит страшновато? На самом деле это просто суперсила, которая делает твой код гибким, как йог. Погнали разбираться!
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 и лямбдами делает работу с ними простой и даже увлекательной.