Определение класса

Инструкция для определения нового класса имеет много необязательных компонентов, которые влияют на его характеристики. В обобщенном виде её можно представить так:

class имя_класса [: модификатор_доступа имя_родительского_класса ...]
{
   [ модификатор_доступа:]
      [описание_члена ...]
   ...   
};

Для простоты изложения мы начнем с минимального достаточного синтаксиса и будем дополнять его по мере продвижения по материалу.

В качестве полигона используем задачу с условной программой для рисования, поддерживающей несколько типов фигур.

Для определения нового класса используется ключевое слово class, после которого идет идентификатор класса и блок кода в фигурных скобках. Как и все инструкции, такое определение требуется завершить точкой с запятой.

Блок кода может быть пустым. Например, компилируемая заготовка класса Shape для программы рисования выглядит так:

class Shape
{
};

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

На этот раз скобки в определении класса задают новый вид контекста — контекст класса. Он является контейнером как для переменных, так и функций, описанных внутри класса.

Описание переменных для хранения свойств класса делается привычными нам инструкциями внутри блока (Shapes1.mq5).

class Shape
{
   int xy;              // координаты центра
   color backgroundColor// цвет заливки
};

Здесь мы задекларировали некоторые поля, о которых рассуждали в теоретических разделах: координаты центра фигуры и цвет заливки.

После такого описания пользовательский тип Shape становится доступным в программе наравне со встроенными типами. В частности, мы можем создать переменную этого типа, и она будет содержать внутри указанные поля. Однако сделать что-либо с ними и даже убедиться, что они там есть, мы пока не можем.

void OnStart()
{
   Shape s;
   // errors: cannot access private member declared in class 'Shape'
   Print(s.x" "s.y);
}

Члены класса по умолчанию являются закрытыми (приватными), и потому из других частей кода, внешних по отношению к классу, обращаться к ним нельзя. Это — принцип инкапсуляции в действии.

Если мы попытаемся вывести фигуру в журнал, результат нас разочарует по нескольким причинам.

Наиболее прямолинейный подход вызовет ошибку "объекты передаются только по ссылке" (мы это видели и со структурами):

Print(s); // 's' - objects are passed by reference only

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

Из раздела про параметры функций (см. раздел Параметры-значения и параметры-ссылки) мы знаем, что для описания ссылок используется символ '&'. Логично было бы предположить, что для получения ссылки на переменную (в данном случае, объект s типа Shape) необходимо поставить тот же знак перед её именем.

Print(&s);

Эта инструкция благополучно компилируется и работает, но делает не совсем то, что ожидалось.

Программа в процессе выполнения выводит некое целое число, например 1 или 2097152 (оно будет, скорее всего, отличаться). Знак амперсанда перед именем переменной означает получение указателя на эту переменную, а не ссылки (в отличие от описания параметра функции).

Указатели будут подробно рассмотрены в отдельном разделе. Пока же отметим, что MQL5 не предоставляет прямого доступа к памяти, и указатель на объект является дескриптором, а по-простому — уникальным номером объекта (он назначается самим терминалом). Но даже если бы указатель вел на адрес в памяти (как это происходит в C++), это не обеспечило бы легального способа прочитать содержимое объекта.

Чтобы выводить содержимое объектов Shape в журнал или еще куда-либо, требуется функция-член класса. Назовем её toString: она должна вернуть строку с неким описанием объекта. Что в него выводить, мы можем решить позднее. Также зарезервируем метод для отрисовки фигуры — draw: пока он выступит декларацией будущего программного интерфейса объекта.

class Shape
{
   int xy;              // координаты центра
   color backgroundColor// цвет заливки
   
   string toString()
   {
      ...
   }
   
   void draw() { /* заглушка будущего интерфейса рисования */ }
};

Определение функций-методов делается привычным способом, с тем лишь отличием, что они находятся внутри блока кода, формирующего класс.

В будущем мы узнаем, как можно разнести объявление функции внутри блока класса и её определение за пределами блока. Этот подход часто используется для вынесения объявлений в заголовочный файл, и "скрытия" определений в mq5-файле. Это делает код более понятным (за счет того, что программный интерфейс представлен отдельно, в компактном виде, без реализации). Кроме того, это позволяет, при необходимости, распространять библиотеки программ в виде ex5-файлов (без основных исходных кодов, но с предоставлением заголовочного файла, который достаточен для вызова методов внешнего интерфейса).

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

string toString()
{
  return (string)x + " " + (string)y;
}

Однако сейчас функции toString и draw такие же закрытые, как и остальные поля. Нам необходимо сделать их доступными извне класса.