- Основы ООП: абстракция
- Основы ООП: инкапусляция
- Основы ООП: наследование
- Основы ООП: полиморфизм
- Основы ООП: композиция (дизайн)
- Определение класса
- Права доступа
- Конструкторы: по умолчанию, параметрический, копирования
- Деструкторы
- Ссылка на себя: this
- Наследование
- Динамическое создание объектов: new и delete
- Указатели
- Виртуальные методы (virtual и override)
- Статические члены
- Вложенные типы, пространства имен и оператор контекста '::'
- Разнесение объявления и определения класса
- Абстрактные классы и интерфейсы
- Перегрузка операторов
- Приведение объектных типов: dynamic_cast и указатель void *
- Указатели, ссылки и const
- Управление наследованием: final и delete
Указатели
Как мы уже говорили в разделе Определение класса, указатели в MQL5 — это некие дескрипторы (уникальные номера) объектов, а не адреса в памяти, как в C++. Для автоматического объекта мы получали указатель, поставив амперсанд перед его именем (в данном контексте, символ амперсанда является оператором "взятия адреса"). Так, в следующем примере переменная p указывает на автоматический объект s.
Shape s; // автоматический объект
|
В предыдущих разделах мы научились получать указатель на объект в результате динамического создания с помощью 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)
|
А вот пример перегрузки оператора '=' в связке с указателями, где наличие или отсутствие '*' меняет поведение:
class Object
|
Скрипт выведет в журнал записи вида:
pointer: 2097152 <- 3145728
|
В русскоязычной литературе по программированию этот оператор также часто называется "разыменованием", но мы уже применили данный термин к оператору '.' для доступа к свойством объекта. Поэтому назовем оператор '*' оператором "овеществления" или "объективации".
Указатели часто используются для взаимной увязки объектов. Проиллюстрируем идею создания подчиненных объектов, получающих указатель на this своего объекта-создателя (ThisCallback.mq5). Мы упоминали этот прием в разделе про ключевое слово this.
Попробуем с помощью него реализовать схему периодического уведомления "создателя" о проценте выполненных вычислений в подчиненном объекте: мы делали её аналог с помощью указателя на функцию. Вычислениями управляет класс Manager, а сами вычисления (вполне вероятно, по разным формулам) производятся в отдельных классах — в данном примере показан один класс Element.
class Manager; // предварительное объявление
|
Подчиненный объект может с помощью полученной ссылки уведомлять "начальника" о ходе работы. Достижение конца вычислений сигнализирует управляющему объекту, что можно удалить объект-вычислитель или дать поработать другому. Конечно, фиксированный массив из одного элемента в классе 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
|
Тогда при создании квадрата установим не только равные размеры сторон, но и передадим typename(this) из класса Square:
class Square : public Rectangle
|
Кроме этого перенесем конструкторы в классе Shape в protected-секцию: это запретит создание объекта Shape самого по себе — он может выступать только в качестве базового для своих классов-наследников.
Порождать фигуры поручим функции addRandomShape, которая возвращает указатель на вновь созданный объект. В демонстрационных целях в ней сейчас будет реализована случайная генерация фигур: их типов, позиций, размеров и цветов.
Типы поддерживаемых фигур сведены в перечисление SHAPES: они соответствуют пяти реализованным классам.
Случайные числа в заданном диапазоне возвращает функция random (в ней используется встроенная функция rand, которая при каждом вызове возвращает случайное целое число в диапазоне от 0 до 32767). Центры фигур генерируются в диапазоне от 0 до 500 пикселей, размеры фигур — в диапазоне до 200. Цвет формируется из трех RGB-составляющих (см. раздел Цвет), каждая в диапазоне от 0 до 255.
int random(int range)
|
Мы генерируем 10 фигур и выводим их в журнал (результат может отличаться из-за случайности выбора типов и свойств). Не забываем удалить объекты с помощью delete, поскольку они создавались динамически (здесь это делается в одном и том же цикле, потому что фигуры далее не используются; в реальной программе массив фигур будет, скорее всего, сохраняться каким-либо образом в файл для последующей загрузки и продолжения работы с изображением).
0: Ellipse 241 38
|
Фигуры успешно создаются и "рапортуют" о своих свойствах.
Теперь все готово для того, чтобы обратиться к прикладному интерфейсу наших классов, то есть методу draw.