11 июня 2012

#8 Указатели на функции, методы и переменные-члены


... или «очаровательные тройняшки с разным характером»


Симпатичные

Указатель на функцию


Объявляется следующим образом:
int (*pFunc)(double);
Дополнительные скобки нужны для того, чтобы дать компилятору понять, что не описывается прототип функции, именуемой pFunc, возвращающей int* и принимающей double. После объявления указателя на функцию, мы получаем переменную с именем pFunc, способную указывать на функции, сигнатура которых соответствует той, которая использовалась при объявлении указателя. Можем так же присвоить указателю нулевое значение. Однако, в отличие от указателя void*, способного ссылаться на любой тип данных, с функциями такого выполнить не удастся — нельзя создать универсальный указатель на функцию, способный ссылаться на любую функцию.
Можно так же объявить ссылку на функцию. Как и обычные ссылки, эта ссылка не может быть переназначена и должна быть инициализирована.
void someFunc(int value);
void (&rFunc)(int) = someFunc;
...

Такой же смысл несет константный указатель на функцию:
void (*const pcFunc)(int) = someFunc;
При присваивании указателю значения, не обязательно брать адрес функции. Компилятор понимает, что вы хотите сделать.
pFunc = someOtherFunc;
pFunc = &someOtherFunc;
Так же можно не разыменовывать указатель, чтобы вызвать функцию:
pFunc(60);
(*pFunc)(60);
Используя typedef можно объявить тип данных указателя на функцию с определённой сигнатурой:
typedef void (*FUNCPTR)(double);
FUNCPTR pFunction = 0;

pFunction является объектом типа указатель на функцию, которая ничего не возвращает и принимает переменную типа double.
Указатели на функцию могут принимать как адреса обычных функций, не являющихся методами классов, так и адреса статических методов классов. Что касается нестатических методов, то их адреса — это не указатели и их нельзя присвоить указателю на функцию.
Можно так же получать адреса перегруженных вариантов функций. Компилятор выберет подходящий вариант по сигнатуре.
Если вам потребуется объявить массив из указателей на функцию, то выглядеть это будет так:
double (*afp[N])(double); // Массив из указателей на функции принимающих

                          // double и возвращающих double, размером N


Указатель на метод класса


При получении адреса нестатического метода класса, получаем не адрес функции, а указатель на метод. Это немного разные вещи. Указатель на метод класса обладает большей информацией, чем простой указатель на функцию, храня при этом информацию о виртуальности функции, её адреса в таблице виртуальных функций, смещению относительно указателя this в объекте и т.п. Важно заметить, что виртуальных указателей на методы не существует. Вся информация о виртуальности принадлежит самому методу.
class Mammal {
public:
  int getAge() const;
  void setAge(int _iNewValue);

  virtual void makeSound();
  // ...

private:
  int m_iAge;
};

class Cat final : public Mammal {
public:
  void chaseMice();
  void makeSound();
};

int (Mammal::*f1)() const = &Mammal::getAge; // Указатель на метод класса Mammal, принимающий
                                             //   void и возвращающий int, инициализированный
                                             //   методом getAge класса Mammal.
Чтобы вызвать метод по указателю, необходим объект, адрес которого будет вычислен для смещения:
Cat cat;
int iAge = (cat.*f1)();    // Вызов метода класса через указатель на него.
Mammal* pMammal = &cat;
iAge = (pMammal->*f1)();   // То же самое, только в качестве объекта выступает
                           //   указатель, который следует сначала разыменовать.
                           //   В данном случае, в переменную iAge оба раза
                           //   запишется одно и то же значение.
void (Mammal::*pSoundFunc)()  = &Mammal::makeSound;
(pMammal->*pSoundFunc)();  // Метод makeSound — виртуальный. Поскольку pMammal
                           //   ссылается на объект, который на самом деле
                           //   является кошкой, то будет вызвана версия метода
                           //   makeSound, определённая в классе Cat.
                           //   Скорее всего, следует ожидать появления на
                           //   экране чего-нибудь очень похожего на "Meow".
Скобки нужны для задания приоритета — сначала разыменовываем нужный метод, после вызываем.
Обычно указатели на методы реализуются как небольшая структура, способная сохранить все необходимые данные.
Указатели на методы контравариантны: компилятор может автоматически преобразовать указатель на метод базового класса в указатель на метод открытого производного класса, но не наоборот:
void (Cat::*pf1)(int) = &Mammal::setAge;   // Тут всё нормально: кошкам можно
                                           //   устанавливать возраст, ведь
                                           //   они —  млекопитающие.
void (Mammal::*pf2)() = &Cat::caseMice;    // Ошибка! Не все млекопитающие
                                           //   могут (и любят, хотят) ловить мышей.

Как уже упоминалось, если потребуется указатель на статический метод класса, следует использовать обычной указатель на функцию, а не указатель на метод класса.


Указатель на переменную-член класса


Такой указатель не содержит адреса, да и поведение у него не как у обычного указателя.
Определим класс A:
class {
public:
  int m_i;
  int m_i2;
  double m_d;
};

Чтобы объявить указатель на переменную член m_i, следует написать:
int A::*pmA; // указатель на переменную член типа int класса A.
В отличие от обычных указателей, такие указатели не ссылаются на конкретную область памяти, не на конкретный член конкретного объекта. Они ссылаются на смещение указанной переменной-члена относительно начала адреса объекта. Т.е. не имеет значения как устроен компилятор, и какое смещение будет для переменной m_i, внутри объектов типа A. То, что оно будет одинаково для всех объектов этого класса — вот что важно. Для использования такого указателя нам потребуется объект, к конкретному значению которого потребуется обратиться.
int A::*pmi = &A::m_i; // Объявляем указатель на переменную-член типа int из класса А
                       //   инициализируем его значением m_i, из класса A.
                       //   А могли бы и значением m_i2 — его сигнатура тоже подходит.
                       //   По сути — задание указателю значения смещения m_i в рамках
                       //   любого объекта типа A.
A a;                   // Создаём простой объект типа A.
a.*pmi = 0;            // Переменной-члену m_i объекта a присваивается значение 0.
A* pa = new A;         // Создаём объект класса A, используя указатель.
pa->*pmi = 17;         // Переменной-члену m_i объекта, на который ссылается указатель
                       //   pa, присваивается значение 17.
Как уже говорилось &A::m_i — это не получение адреса переменной, а всего лишь получение её смещения в рамках объекта, поскольку эта переменная не статическая, а уникальная для каждого объекта. При подобной записи для статической переменной, мы бы получили её адрес.
Записывая pa->*pmi, мы увеличиваем значение адреса, хранящегося в pa, на значение, которое означает «смещение переменной-члена m_i относительно начала объекта». Таким образом, мы получаем адрес конкретной переменной конкретного объекта. То же самое происходит и в случае обращения через объект: a.*pmi, с той лишь разницей, что компилятор сам применяет оператор взятия адреса у объекта a.
Оператор разыменования указателя на переменную член может использоваться как для объектов (.*), так и для указателей на (->*).

Так же, как и в случае с указателями на методы класса, имеет место контравариантность:
Существует неявное преобразование указателя на переменную-член базового класса в указатель на переменную-член открытого производного класса, но не наоборот, как с указателями на объекты любого из базовых и производного классов.
Смещение объекта в базовом классе действительно и для объектов открытого производного класса, поскольку объект производного класса является одновременно так же и объектом базового класса. Однако в базовом классе нет переменных членов, присутствующих в производном.
class Mammal {
public:
  int m_iAge;
};

class Dog final : public Mammal {
public:
  std::string m_sBreed;
};

int Dog::*pmi = &Mammal::m_iAge; // Всё нормально. В объекте класса собаки есть возраст.
std::string Mammal::*pms = &Dog::m_sBreed; // Ошибка. Животное понятия не имеет
                                           //   о наличии породы у собаки.

Такое приведение (правильное) поддерживается компилятором неявно, и он может привести самостоятельно указатель на переменную-член базового класса к указателю на переменную-член открытого производного класса (так же, как и Dog* к Mammal*).