- Основы ООП: абстракция
- Основы ООП: инкапусляция
- Основы ООП: наследование
- Основы ООП: полиморфизм
- Основы ООП: композиция (дизайн)
- Определение класса
- Права доступа
- Конструкторы: по умолчанию, параметрический, копирования
- Деструкторы
- Ссылка на себя: this
- Наследование
- Динамическое создание объектов: new и delete
- Указатели
- Виртуальные методы (virtual и override)
- Статические члены
- Вложенные типы, пространства имен и оператор контекста '::'
- Разнесение объявления и определения класса
- Абстрактные классы и интерфейсы
- Перегрузка операторов
- Приведение объектных типов: dynamic_cast и указатель void *
- Указатели, ссылки и const
- Управление наследованием: final и delete
Перегрузка операторов
В главе Выражения мы изучили множество операций, определенных для встроенных типов. Например, для переменных типа double мы могли вычислить выражение:
double a = 2.0, b = 3.0, c = 5.0;
|
Было бы удобно использовать похожий синтаксис при работе с пользовательскими типами, например, матрицами:
Matrix a(3, 3), b(3, 3), c(3, 3); // создаем матрицы 3x3
|
MQL5 предоставляет такую возможность за счет перегрузки операторов.
Данный технический прием организуется путем описания методов с именем, начинающимся с ключевого слова operator, и содержащего далее символ (или последовательность символов) одной из поддерживаемых операций. В обобщенном виде это можно представить так:
тип_результата operator@ ( [тип имя_параметра] ); |
Здесь @ - символ(ы) операций.
Полный перечень операций MQL5 был приведен в разделе Приоритеты операций, однако не все они разрешены для перегрузки.
Запрещены для перегрузки:
- двоеточия '::', разрешение контекста;
- круглые скобки '()', "вызов функции" или "группировка";
- точка '.', "разыменование";
- амперсанд '&', "взятие адреса", унарный оператор (однако доступен амперсанд как бинарный оператор "побитовое И");
- условный тернарный '?:';
- запятая ','.
Все остальные операторы доступны для перегрузки. Приоритеты операций при перегрузке нельзя изменить, они остаются равными стандартным приоритетам, поэтому следует, при необходимости, использовать группировку с помощью скобок.
Создать перегрузку для какого-то нового символа, не входящего в стандартный перечень, нельзя.
Все операторы перегружаются с учетом их унарности и бинарности, то есть количество требуемых операндов сохраняется. Как любой метод класса, перегрузка оператора может вернуть значение некоторого типа. При этом сам тип следует выбирать исходя из планируемой логики использования результата функции в выражениях (см. далее).
Методы перегрузки операторов имеют следующий вид (вместо символа '@' подставляется символ(ы) требуемого оператора):
Название |
Заголовок метода |
Использование |
Функциональный |
---|---|---|---|
унарные префиксные |
тип operator@() |
@object |
object.operator@() |
унарные постфиксные |
тип operator@(int) |
object@ |
object.operator@(0) |
бинарные |
тип operator@(тип имя_параметра) |
object@argument |
object.operator@(argument) |
индекс |
тип operator[](тип имя_индекса) |
object[argument] |
object.operator[](argument) |
Унарные операторы не принимают параметров. Из унарных только операторы инкремента '++' и декремента '--' поддерживают постфиксную форму в дополнение к префиксной, все остальные унарные операторы — только префиксные. Указание анонимного параметра типа int используется для обозначения постфиксной формы (чтобы отличить от префиксной), но сам параметр игнорируется.
Бинарные операторы должны принимать один параметр. Для одного и того же оператора возможно несколько перегруженных вариантов с параметром разного типа, включая и такой же тип, как класс текущего объекта. При этом объекты в качестве параметров могут передаваться только со ссылке или по указателю (последнее — только для объектов классов, но не структур).
Перегруженные операторы можно применять как с помощью синтаксиса операций в составе выражений (ради чего, собственно, и делается перегрузка), так и синтаксиса вызовов методов — оба варианта приведены в таблице выше. Функциональный эквивалент делает более очевидным, что с технической точки зрения оператор — не более чем вызов метода для объекта, причем для префиксных операторов объект стоит справа, а для всех остальных — слева от символа. Методу бинарного оператора будет передаваться в качестве аргумента то значение или выражение, что стоит справа от оператора (это может быть, в частности, другой объект или переменная встроенного типа).
Отсюда следует, что перегруженные операторы не обладают свойством коммутативности: a@b в общем случае не равно b@a, потому что для a оператор @ может быть перегружен, а для b — нет. Более того, если b является переменной или значением встроенного типа, то для него в принципе нельзя перегрузить стандартное поведение.
В качестве первого примера рассмотрим класс Fibo для генерации чисел из ряда Фибоначчи (мы уже делали одну реализацию данной задачи с помощью функций, см. Определение функции). В классе предусмотрим 2 поля для хранения текущего и предыдущего числа ряда: current и previous соответственно. Конструктор по умолчанию будет их инициализировать значениями 1 и 0. Также предусмотрим конструктор копирования (FiboMonad.mq5).
class Fibo
|
Начальное состояние объекта: текущее число 1, предыдущее — 0. Для нахождения следующего числа в ряду перегрузим префиксные и постфиксные операции инкремента.
Fibo *operator++() // prefix
|
Обратите внимание, что префиксный метод возвращает указатель не текущий объект Fibo после модификации числа, а постфиксный — на новый объект с сохраненным предыдущим счетчиком, что соответствует принципам работы постфиксного инкремента.
При необходимости, программист, конечно, может перегрузить любую операцию произвольным образом. Например, никто не запрещает в реализации инкремента вычислять произведение, выводить число в журнал или делать что-то еще. Однако рекомендуется придерживаться подхода, когда перегрузка операции выполняет интуитивно понятные действия.
Операции декремента реализуем по похожему принципу: они будут возвращать предыдущее число ряда.
Fibo *operator--() // prefix
|
Для получения числа из ряда по заданному номеру перегрузим операцию доступа по индексу.
Fibo *operator[](int index)
|
Для получения текущего числа, содержащегося в переменной current, перегрузим оператор '~' (как редко используемый).
int operator~() const
|
Без этой перегрузки потребовалось бы все равно реализовать какой-либо публичный метод для чтения закрытого поля current. Мы воспользуемся этим оператором для вывода чисел с помощью Print.
Также для удобства следует перегрузить присваивание.
Fibo *operator=(const Fibo &other)
|
Проверим, как это все работает.
void OnStart()
|
Результаты совпадают с ожидаемыми. Осталось рассмотреть один нюанс.
Fibo f5;
|
Перегрузка оператора присваивания для указателя работает только при обращении через объект. Если обращение идет через указатель, то происходит стандартное присваивание одного указателя другому.
В качестве возвращаемого типа перегруженного оператора можно использовать один из встроенных типов, объектный тип (класса или структуры) или указатель (только для объектов класса).
Для возврата объекта (экземпляра, а не ссылки) в классе должен быть реализован конструктор копирования. Такой способ вызовет дублирование экземпляра, что может сказаться на эффективности кода. По возможности следует возвращать указатель.
Вместе с тем, при возврате указателя нужно убедиться, что возвращается не локальный автоматической объект (который будет удален при выходе из функции, и указатель станет недействительным), а какой-либо уже существующий — как правило, возвращается &this.
Возврат объекта или указателя на объект позволяет "отправлять" результат работы одного перегруженного оператора в другой, и тем самым конструировать сложные выражения по аналогии с тем, как мы привыкли делать со встроенными типами. Возврат void приведет к невозможности использовать оператор в выражениях. Например, если оператор '=' будет определен с типом void, то перестанет работать множественное присваивание:
Type x, y, z = 1; // конструкторы и инициализация переменных некоего класса
|
Цепочка присваиваний выполняется справа налево, а y = z вернет пустоту.
Если объекты содержат поля только встроенных типов (включая массивы), то оператор присваивания/копирования '=' из объектов того же класса переопределять не обязательно: MQL5 по умолчанию обеспечивает копирование всех полей "один в один". Не следует путать оператор присваивания/копирования с конструктором копирования и инициализацией.
Теперь обратимся ко второму примеру: работе с матрицами (Matrix.mq5).
Отметим, между прочим, что недавно в MQL5 появились встроенные объектные типы матриц и векторов. Использовать ли встроенные типы или свои (а может быть, комбинировать их) — выбор каждого разработчика. Готовая и быстрая реализация многих востребованных методов во встроенных типах удобна и избавляет от рутинного кодирования. С другой стороны, собственные классы позволяют адаптировать алгоритмы под свои задачи. Здесь мы приводим класс Matrix в качестве учебного пособия.
В классе матрицы обеспечим хранение её элементов в одномерном динамическом массиве m. Под размеры выделим переменные rows и columns.
class Matrix
|
Основной конструктор принимает два параметра (размеры матрицы) и выделяет память под массив. Также имеется конструктор копирования из другой матрицы other. Здесь и далее массово используются встроенные функции для работы с массивами (в частности, ArrayCopy, ArrayResize, ArrayInitialize) — они будут рассмотрены в отдельной главе.
Заполнение элементов организуем из внешнего массива, перегрузив оператор присваивания:
Matrix *operator=(const double &a[])
|
Для реализации сложения двух матриц перегрузим операции '+=' и '+':
Matrix *operator+=(const Matrix &other)
|
Обратите внимание, что оператор '+=' возвращает указатель на текущий объект после его модификации, а оператор '+' — новый экземпляр по значению (будет использован конструктор копирования), причем сам оператор имеет модификатор const, так как не меняет текущий объект.
Оператор '+' является по сути оберткой, которая делегирует всю работу оператору '+=', предварительно создав для его вызова временную копию текущей матрицы под именем temp. Таким образом, temp суммируется с other при помощи внутреннего вызова оператора '+=' (при этом temp модифицируется) и затем возвращается как результат оператора '+'.
Умножение матриц перегружается аналогично, с помощью двух операторов '*=' и '*'.
Matrix *operator*=(const Matrix &other)
|
Наконец, умножение матрицы на число:
Matrix *operator*=(const double v)
|
Для сравнения двух матриц предоставим операторы '==' и '!=':
bool operator==(const Matrix &other) const
|
Для отладочных целей реализуем вывод массива матрицы в журнал.
void print() const
|
Кроме описанных перегрузок в классе Matrix дополнительно имеется перегрузка оператора []: он возвращает объект вложенного класса MatrixRow, то есть строку с заданным номером.
MatrixRow operator[](int r)
|
Сам класс MatrixRow обеспечивает более "глубокий" доступ к элементам матрицы путем перегрузки того же оператора [] (то есть для матрицы можно будет естественным образом указать два индекса m[i][j]).
class MatrixRow
|
Оператор [] для параметра типа int возвращает объект класса MatrixElement, через который можно записывать конкретный элемент в массиве. Для чтения элемента применяется оператор [] с параметром типа uint. Это похоже на трюк, но таково ограничение языка: перегрузки должны отличаться типом параметра. В качестве альтернативы для чтения элемента в классе MatrixElement предусмотрена перегрузка оператора '~'.
При работе с матрицами часто нужна единичная, поэтому создадим для неё производный класс:
class MatrixIdentity : public Matrix
|
Теперь попробуем матричные выражения в действии.
void OnStart()
|
Здесь мы создали 2 матрицы размером 3 на 2 и 2 на 3 соответственно, затем заполнили их значениями из массивов и отредактировали выборочный элемент с помощью синтаксиса двух индексов [][]. Наконец вычислили выражение m * n + p, где все операнды — это матрицы. Строкой ниже показано то же самое выражение в форме вызовов методов. Результаты совпадают.
В MQL5, в отличие от C++, не поддерживается перегрузка операторов на глобальном уровне. В MQL5 можно перегрузить оператор только в контексте класса или структуры, то есть с помощью их метода. Также MQL5 не поддерживает перегрузку приведения типов, операторов new и delete.