В C++ существуют не только обычные указатели, которые хранят адрес объекта в памяти. Есть и более специфический инструмент — указатели на члены класса. Это особый вид указателей, который "указывает" не на конкретный объект, а на само поле или метод внутри определения класса.
В отличие от обычного указателя, который хранит абсолютный адрес в памяти, указатель на член класса хранит, по сути, смещение (offset) поля данных или адрес функции-члена относительно начала любого объекта этого класса. Сами по себе они бесполезны; их сила раскрывается только при применении к конкретному экземпляру класса. Посмотрим, как они работают.
Указатели на члены класса не часто используются, но в некоторых сценариях они незаменимы. Некоторые ситуации применения:
Реализация паттерна "Стратегия" или "Команда": объект может хранить указатель на метод, который определяет его текущее поведение (стратегию)
Обработка данных по шаблону: можно создать массив указателей на поля и итерироваться по ним для выполнения однотипных операций (сериализация, валидация, вычисление суммы).
Диспетчеризация вызовов: в событийных системах, управляемых событиями, можно составить карту.словарь (например, std::map), где ключом является некое событие, а значением — указатель на метод-обработчик этого события.
Указатели на поля класса/структуры представляют простейший вид указателей на члены класса. Они позволяют выбрать, с каким полем объекта работать, уже во время выполнения программы. Их формальный синтаксис вовлекает три компонента:
Объявление
тип_поля Класс::*имя_указателя;
Инициализация
имя_указателя = &Класс::имя_поля;
Использование
объект.*имя_указателя (оператор "точка-звездочка") указатель_на_объект->*имя_указателя (оператор "стрелка-звездочка")
Рассмотрим простейший пример:
#include <iostream>
#include <string>
struct Person{
std::string name;
int age;
};
int main() {
// Объявляем указатель на член класса Person типа int
int Person::*p_age;
// Инициализируем указатель - указываем на поле age
p_age = &Person::age;
// Используем указатель
Person tom{"Tom", 41}; // объект для примера
std::cout << "Person age: " << tom.*p_age << std::endl; // Person age: 41
// Для примера используем указатель через указатель на объект
Person bob{"Bob", 46};
Person* p_bob = &bob;
std::cout << "Person age: " << p_bob->*p_age << std::endl; // Person age: 46
return 0;
}
Рассмотрим по этапно. Сначала объявляем указатель на член класса Person типа int, который называется "p_age":
int Person::*p_age;
Далее инициализируем указатель - он указывает на поле age:
p_age = &Person::age;
Далее используем указатель для обращения к полям объекта. Для примера я использовал доступ как через объект, так и через указатель на объект:
Person tom{"Tom", 41}; // объект для примера
std::cout << "Person age: " << tom.*p_age << std::endl; // Person age: 41
Использование указателей в данном примере, конечно, избыточно. Поэтому рассмотрим еще несколько примеров.
Допустим, у нас будет класс Product с двумя числовыми полями: цена и скидка:
#include <iostream>
#include <string>
struct Product {
std::string name;
double price;
double min_discount;
double max_discount;
void print_final_price(double Product::*discount) {
// Применяем указатель на член к this
double final_price = this->price - (this->*discount);
std::cout << "Final price for " << this->name << " is: " << final_price << std::endl;
}
};
int main() {
Product phone{"Smartphone", 1000.0, 50.0, 200.0};
// В зависимости от условия, наш указатель будет "смотреть" на разные поля
std::cout << "Min discount:" << std::endl;
phone.print_final_price(&Product::min_discount); // 1000 - 50 = 950
std::cout << "Max discount:" << std::endl;
phone.print_final_price(&Product::max_discount); // 1000 - 200 = 800
phone.max_discount = 250; // Меняем скидку для демонстрации
phone.print_final_price(&Product::max_discount); // 1000 - 250 = 750
return 0;
}
Здесь цель — динамически выбрать, какое из числовых полей Product использовать для вычисления скидки (max_discount или min_discount).
Метод print_final_price класса Product принимает указатель на член:
void print_final_price(double Product::*discount) {
double final_price = this->price - (this->*discount);
std::cout << "Final price for " << this->name << " is: " << final_price << std::endl;
}
В качестве параметра в функцию передается указатель double Product::*discount. Он не хранит значение, а лишь "знает", как получить доступ к полю типа double внутри
любого объекта Product.
Внутри метода print_final_price используется оператор .* в выражении this->*discount. Это позволяет применить переданный указатель к текущему объекту (this).
В функции main при вызове phone.print_final_price(&Product::min_amount) мы передаем "адрес" поля min_discount:
phone.print_final_price(&Product::min_discount);
В результате метод вычисляет price - min_discount. Если бы мы передали указатель на другое совместимое поле, расчет был бы иным.
Это позволяет методу print_final_price быть универсальным, не зная заранее, какое именно значение нужно вычесть из цены.
Причем мы можем динамически изменить значение поля, и тогда результат вычисления изменится, но в методе будет использоваться то же самое поле:
phone.max_discount = 250; // Меняем скидку для демонстрации phone.print_final_price(&Product::max_discount); // 1000 - 250 = 750
Аналогично мы можем динамически присваивать значение указателю:
#include <iostream>
#include <string>
struct Product {
std::string name;
double price;
double min_discount;
double max_discount;
};
int main() {
// Объявляем указатель на член класса Product типа double
double Product::*discount;
Product phone{"Smartphone", 1000.0, 50.0, 200.0};
// В зависимости от условия, наш указатель будет "смотреть" на разные поля
if (phone.price > 500) {
// Указываем на поле min_discount
discount = &Product::min_discount;
} else {
// Указываем на поле max_discount
discount = &Product::max_discount;
}
// используем указатель
std::cout << "Final discount:" << phone.*discount << std::endl; // Final discount:50
phone.min_discount = 80.0; // меняем значение
std::cout << "Final discount:" << phone.*discount << std::endl; // Final discount:80
return 0;
}
И рассмотрим еще один пример:
#include <iostream>
#include <string>
struct Person {
std::string name;
int age;
int salary; // зарплата
int rental_income; // доход от недвижимости
int other_income; // какой-то другой доход
};
int main() {
Person tom{"Tom", 41, 4500, 1500, 400};
// массив указателей
int Person::*required_fields[] = {
&Person::salary,
&Person::rental_income,
&Person::other_income,
};
int total_income = 0;
for (auto income : required_fields) {
total_income += tom.*income;
}
std::cout << "Total income: " << total_income << std::endl; // Total income: 6400
return 0;
}
Здесь определена структура Person, которая представляет человека и которая определяет salary (зарплата), rental_income (доход от недвижимости), other_income (какой-то другой доход).
В функции main создаeтся массив указателей на члены класса Person с именем required_fields. Этот массив инициализируется указателями на три поля: salary, rental_income и
other_income. По сути, мы создали список полей, которые хотим обработать.
int Person::*required_fields[] = {
&Person::salary,
&Person::rental_income,
&Person::other_income,
};
Далее в цикле вычисляем суммарный доход человека, используя поля в этом массиве:
int total_income = 0;
for (auto income : required_fields) {
total_income += tom.*income;
}
Цикл for перебирает каждый элемент в массиве required_fields. В каждой итерации income становится одним из указателей на член (&Person::salary, затем &Person::rental_income и т.д.). И значение соответствующего поля добавляется к переменной total_income.
Указатели на функции позволяют динамически выбирать, какой метод объекта будет вызван. Синтаксис здесь немного сложнее из-за необходимости указывать полную сигнатуру функции:
Объявление:
возвращаемый_тип (Класс::*имя_указателя)(параметры);
Инициализация:
имя_указателя = &Класс::имя_метода;
Использование:
(объект.*имя_указателя)(аргументы); (указатель_на_объект->*имя_указателя)(аргументы);
Причем скобки вокруг (объект.*имя_указателя) обязательны из-за низкого приоритета операторов .* и ->*.
Рассмотрим простейший пример:
Добавим в наш класс `Product` разные способы расчета скидки.
#include <iostream>
#include <string>
struct Product {
std::string name;
double price;
double get_fixed_discount(double amount) {
return amount;
}
double get_percentage_discount(double percent) {
return price * (percent / 100.0);
}
};
int main() {
// Объявляем указатель на метод класса Product, который принимает double и возвращает double
double (Product::*p_discount_calculator)(double);
Product tv{"Big TV", 2000.0};
bool holiday_season = true;
if (holiday_season) {
// Указываем на метод, считающий скидку в процентах
p_discount_calculator = &Product::get_percentage_discount;
double discount = (tv.*p_discount_calculator)(15.0); // Вызываем get_percentage_discount(15.0)
std::cout << "Holiday discount: " << discount << std::endl; // 300
std::cout << "Final price: " << tv.price - discount << std::endl; // 1700
} else {
// Указываем на метод с фиксированной скидкой
p_discount_calculator = &Product::get_fixed_discount;
double discount = (tv.*p_discount_calculator)(100.0); // Вызываем get_fixed_discount(100.0)
std::cout << "Regular discount: " << discount << std::endl; // 100
std::cout << "Final price: " << tv.price - discount << std::endl; // 1900
}
return 0;
}
В данном случае в класс Product добавлены разные способы расчета скидки:
double get_fixed_discount(double amount) {
return amount;
}
double get_percentage_discount(double percent) {
return price * (percent / 100.0);
}
В функции main объявляем указатель на метод класса Product, который принимает double и возвращает double
double (Product::*p_discount_calculator)(double);
Далее с помощью условной конструкции динамически инициализиурем этот указатель и применяем его для вычисления скидки:
if (holiday_season) {
p_discount_calculator = &Product::get_percentage_discount;
double discount = (tv.*p_discount_calculator)(15.0); // Вызываем get_percentage_discount(15.0)
..............
} else {
p_discount_calculator = &Product::get_fixed_discount;
double discount = (tv.*p_discount_calculator)(100.0); // Вызываем get_fixed_discount(100.0)
......................
}