Указатели

Как мы уже говорили в разделе Определение класса, указатели в MQL5 — это некие дескрипторы (уникальные номера) объектов, а не адреса в памяти, как в C++. Для автоматического объекта мы получали указатель, поставив амперсанд перед его именем (в данном контексте, символ амперсанда является оператором "взятия адреса"). Так, в следующем примере переменная p указывает на автоматический объект s.

Shape s;        // автоматический объект
Shape *p = &s;  // указатель на тот же объект
s.draw();       // вызываем метод объекта
p.draw();       // делаем то же самое

В предыдущих разделах мы научились получать указатель на объект в результате динамического создания с помощью new. При этом для получения дескриптора амперсанд не нужен: значение указателя и есть дескриптор.

MQL5 API предоставляет функцию GetPointer, которая выполняет то же действие, что и оператор амперсанд '&', то есть возвращает указатель на объект:

void *GetPointer(Class object);

Какой именно из двух вариантов использовать — дело вкуса.

Обратиться к методам и свойствам объекта по указателю можно с помощью оператора разыменования ('.'). Однако для написания отказоустойчивой программы следует проверять работоспособность указателя перед его разыменованием. Это можно сделать как с помощью функции CheckPointer, рассмотренной в предыдущем разделе, так и в более краткой записи условных операторов сравнения с NULL — if(pointer != NULL) или просто if(pointer). Важно, что сравнение с NULL является менее строгой проверкой, чем вызов CheckPointer, поскольку подразумевает, что любые ненулевые указатели валидны (а это может быть не так, если объект уже удален). Однако такой способ проверки работает быстрее, чем вызов CheckPointer. Кстати говоря, оператор вида if(pointer) также приводит к неявному вызову CheckPointer и возвращает результат её работы.

В некоторых случаях, например, при передаче в функцию параметра-ссылки на объект (именно ссылки, а не указателя!) или для правильного вызова перегруженного оператора может потребоваться преобразовать указатель в объект. Для этой цели применяют унарный оператор "звёздочка" перед указателем. Например:

void Function(const Object &object)
{
   ...
}
 
void OnStart()
{
   Object automatic;
   Object *dynamic = ...; // описание и инициализация
   ...
   Function(automatic);
   Function(*dynamic);    // из указателя в объект, хотя здесь '*' можно опустить
   ...
}

А вот пример перегрузки оператора '=' в связке с указателями, где наличие или отсутствие '*' меняет поведение:

class Object
{
public:
   void operator=(const Object *other)
   {
      Print("pointer: ", &this" <- "other);
   }
   void operator=(const Object &other)
   {
      Print("reference: ", &this" <- ", &other);
   }
};
 
void OnStart()
{
   Object *dynamic1 = new Object();
   Object *dynamic2 = new Object();
   *dynamic1 = dynamic2;   // operator=(const Object *other)
   *dynamic1 = *dynamic2;  // operator=(const Object &other)
   dynamic1 = *dynamic2;   // operator=(const Object &other)
   dynamic1 = dynamic2;    // ни один из методов не вызывается, причем указатель в dynamic1 потерян,
                           // т.к. перезаписан указателем из dynamic2!
   ...
}

Скрипт выведет в журнал записи вида:

pointer: 2097152 <- 3145728
reference: 2097152 <- 3145728
reference: 2097152 <- 3145728

В русскоязычной литературе по программированию этот оператор также часто называется "разыменованием", но мы уже применили данный термин к оператору '.' для доступа к свойством объекта. Поэтому назовем оператор '*' оператором "овеществления" или "объективации".

Указатели часто используются для взаимной увязки объектов. Проиллюстрируем идею создания подчиненных объектов, получающих указатель на this своего объекта-создателя (ThisCallback.mq5). Мы упоминали этот прием в разделе про ключевое слово this.

Попробуем с помощью него реализовать схему периодического уведомления "создателя" о проценте выполненных вычислений в подчиненном объекте: мы делали её аналог с помощью указателя на функцию. Вычислениями управляет класс Manager, а сами вычисления (вполне вероятно, по разным формулам) производятся в отдельных классах — в данном примере показан один класс Element.

class Manager// предварительное объявление
   
class Element
{
   Manager *owner// указатель
   
public:
   Element(Manager &t): owner(&t) { }
   
   void doMath()
   {
      const int N = 1000000;
      for(int i = 0i < N; ++i)
      {
         if(i % (N / 20) == 0)
         {
            // передаем себя в метод управляющего класса
            owner.progressNotify(&thisi * 100.0f / N);
         }
         // ... массивные вычисления
      }
   }
   
   string getMyName() const
   {
      return typename(this);
   }
};
   
class Manager
{
   Element *elements[1]; // массив указателей (1 для демо)
   
public:
   Element *addElement()
   {
      // находим пустой слот в массиве
      // ...
      // передаем себя в конструктор подчиненного класса
      elements[0] = new Element(this); // динамическое создание объекта
      return elements[0];
   }
   
   void progressNotify(Element *econst float percent)
   {
      // Manager выбирает способ уведомления пользователя:
      // вывод на экран, печать, отправка в Internet
      Print(e.getMyName(), "="percent);
   }
};

Подчиненный объект может с помощью полученной ссылки уведомлять "начальника" о ходе работы. Достижение конца вычислений сигнализирует управляющему объекту, что можно удалить объект-вычислитель или дать поработать другому. Конечно, фиксированный массив из одного элемента в классе Manager выглядит не очень серьезно, но в качестве демонстрации он передает суть. Менеджер не только управляет раздачей вычислительных задач, но и предоставляет абстрактный уровень для уведомления пользователя: вместо вывода в журнал он может писать сообщения в отдельный файл, выводить на экран или отправлять в Интернет.

Кстати, обратите внимание на предварительное объявление класса Manager перед определением класса Element. Оно нужно, чтобы описать в классе Element указатель на класс Manager, который определен ниже по коду. Если предварительное объявление опустить, получим ошибку "'Manager' - неизвестный идентификатор, вероятно отсутствует тип?" ("'Manager' - unexpected token, probably type is missing?").

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

Фундаментальным свойством указателей является то, что указатель на базовый класс может использоваться для указания на объект любого производного класса. Это одно из проявлений полиморфизма. Данное поведение возможно потому, что производные объекты содержат как матрешки встроенные "под-объекты" родительских классов.

В частности, для нашей задачи с фигурами легко описать динамический массив указателей Shape и добавлять в него объекты разных типов по запросу пользователя.

Количество классов расширим до пяти (Shapes2.mq5). Помимо Rectangle и Ellipse добавим Triangle, а также сделаем производный от Rectangle класс для квадрата (Square), и производный от Ellipse класс для круга (Circle). Очевидно, что квадрат представляет собой прямоугольник с равными сторонами, а круг — это эллипс с равными большим и малым радиусами.

Для передачи строкового имени класса по цепочке наследования добавим в protected-секциях классов Rectangle и Ellipse специальные конструкторы с дополнительным строковым параметром t:

class Rectangle : public Shape
{
protected:
   Rectangle(int pxint pyint sxint sycolor backstring t) :
      Shape(pxpybackt), dx(sx), dy(sy)
   {
   }
   ...
};

Тогда при создании квадрата установим не только равные размеры сторон, но и передадим typename(this) из класса Square:

class Square : public Rectangle
{
public:
   Square(int pxint pyint sxcolor back) :
      Rectangle(pxpysxsxbacktypename(this))
   {
   }
};

Кроме этого перенесем конструкторы в классе Shape в protected-секцию: это запретит создание объекта Shape самого по себе — он может выступать только в качестве базового для своих классов-наследников.

Порождать фигуры поручим функции addRandomShape, которая возвращает указатель на вновь созданный объект. В демонстрационных целях в ней сейчас будет реализована случайная генерация фигур: их типов, позиций, размеров и цветов.

Типы поддерживаемых фигур сведены в перечисление SHAPES: они соответствуют пяти реализованным классам.

Случайные числа в заданном диапазоне возвращает функция random (в ней используется встроенная функция rand, которая при каждом вызове возвращает случайное целое число в диапазоне от 0 до 32767). Центры фигур генерируются в диапазоне от 0 до 500 пикселей, размеры фигур — в диапазоне до 200. Цвет формируется из трех RGB-составляющих (см. раздел Цвет), каждая в диапазоне от 0 до 255.

int random(int range)
{
   return (int)(rand() / 32767.0 * range);
}
   
Shape *addRandomShape()
{
   enum SHAPES
   {
      RECTANGLE,
      ELLIPSE,
      TRIANGLE,
      SQUARE,
      CIRCLE,
      NUMBER_OF_SHAPES
   };
   
   SHAPES type = (SHAPES)random(NUMBER_OF_SHAPES);
   int cx = random(500), cy = random(500), dx = random(200), dy = random(200);
   color clr = (color)((random(256) << 16) | (random(256) << 8) | random(256));
   switch(type)
   {
      case RECTANGLE:
         return new Rectangle(cxcydxdyclr);
      case ELLIPSE:
         return new Ellipse(cxcydxdyclr);
      case TRIANGLE:
         return new Triangle(cxcydxclr);
      case SQUARE:
         return new Square(cxcydxclr);
      case CIRCLE:
         return new Circle(cxcydxclr);
   }
   return NULL;
}
   
void OnStart()
{
   Shape *shapes[];
   
   // имитируем создание произвольных фигур пользователем
   ArrayResize(shapes10);
   for(int i = 0i < 10; ++i)
   {
      shapes[i] = addRandomShape();
   }
   
   // обрабатываем фигуры: пока просто вывод в лог   
   for(int i = 0i < 10; ++i)
   {
      Print(i": "shapes[i].toString());
      delete shapes[i];
   }
}

Мы генерируем 10 фигур и выводим их в журнал (результат может отличаться из-за случайности выбора типов и свойств). Не забываем удалить объекты с помощью delete, поскольку они создавались динамически (здесь это делается в одном и том же цикле, потому что фигуры далее не используются; в реальной программе массив фигур будет, скорее всего, сохраняться каким-либо образом в файл для последующей загрузки и продолжения работы с изображением).

0: Ellipse 241 38
1: Rectangle 10 420
2: Circle 186 38
3: Triangle 27 225
4: Circle 271 193
5: Circle 293 57
6: Rectangle 71 424
7: Square 477 46
8: Square 366 27
9: Ellipse 489 105

Фигуры успешно создаются и "рапортуют" о своих свойствах.

Теперь все готово для того, чтобы обратиться к прикладному интерфейсу наших классов, то есть методу draw.