От начального до среднего уровня: Классы (I)
Введение
В предыдущей статье «От базового к среднему уровню: очереди, списки и деревья (V)» мы рассмотрели, как реализовать древовидную структуру. Однако, прежде чем вернуться к этой теме, мы должны рассмотреть один вопрос, который я до сих пор откладывал. Но мы больше не можем откладывать его: не объяснив, как работает объектно-ориентированное программирование, трудно объяснить необходимые структуры и компоненты. Хотя в последних статьях я уже использовал средства этой парадигмы, не объясняя их, предыдущий материал можно было понять и без слишком глубокого вникания в детали.
Следующие статьи потребуют более глубокого понимания этой парадигмы. Если вы хорошо поняли предыдущую статью, то, вероятно, заметили, что MetaTrader 5 постоянно выводит предупреждения о неудалённых экземплярах или неосвобождённой памяти. Хотя они доставляют неудобства и нам приходится корректировать код, чтобы их устранить, для этого необходимо понимать, как работает объектно-ориентированное программирование. Поэтому мы временно отложим эту тему, чтобы объяснить хотя бы её основы.
Без лишних предисловий перейдём к основной теме.
Классы (I)
Как следует из названия, здесь мы рассмотрим классы — одно из фундаментальных понятий объектно-ориентированного программирования. В этом первом обзоре мы рассмотрим только то, что необходимо для понимания следующих статей. Пока мы не будем углубляться во все аспекты классов, поскольку некоторые из них уже были рассмотрены в статьях о структурном программировании. Обратитесь к предыдущим статьям за более подробной информацией.
Тем не менее, нам следует должным образом объяснить некоторые особенности и возможности классов. Понимание этих принципов позволит вам понять, почему приведённые далее фрагменты кода работают и как их можно использовать. Я хочу, чтобы вы могли разбираться в них при минимуме пояснений с моей стороны, потому что я буду постепенно сокращать количество комментариев.
Начнём с основ: чтобы понять классы, сначала нужно вспомнить другое понятие из предыдущих статей — структуру.
В статье "От начального к среднему: Struct (I)" мы представили структуры данных. Структуры позволяют объединять связанные между собой переменные в один тип, определённый программистом. В других статьях также объяснялось их использование, но именно эта статья положила начало серии, посвящённой исключительно данной теме. Цель заключалась в том, чтобы познакомить вас со структурным программированием.
Изучение структурного программирования значительно облегчает понимание классов. Когда появились классы, тщательное изучение структурного программирования уже показало, какие аспекты необходимо было улучшить. Объектно-ориентированное программирование возникло как естественное развитие. Поэтому, если вы ещё не до конца усвоили понятие структуры, я советую вам перечитать предыдущие статьи. Объяснение опирается на эти знания.
В нескольких статьях о структурах я показал, что мы можем писать структурированный код, и объяснил происхождение этого термина. Однако, если вы спокойно изучили эту тему и писали структурный код, возможно, вы заметили небольшую проблему. Эта модель позволяет разрабатывать код гораздо большей сложности по сравнению со старым подходом к программированию, в котором не существует понятия структуры данных. У структуры есть одно конкретное ограничение: её переменные не всегда инициализируются правильно.
"Подождите минутку. Не понимаю. Какая трудность возникает при инициализации переменных структуры? Достаточно объявить подпрограмму и вызвать её перед использованием этих переменных. Я не вижу здесь никакой трудности". На первый взгляд вы правы, уважаемый читатель. Это правильное решение, и ваше замечание свидетельствует о том, что вы внимательно изучили и поняли предыдущие статьи.
Однако подпрограмма инициализации может не выполниться. Такие сбои случаются редко, но затрудняют разработку. Гораздо чаще мы забываем вызвать подпрограмму, которая инициализирует переменные. В таких случаях структура теряет надёжность и нуждается в доработке.
Разберём самую распространённую ошибку: не вызвать подпрограмму, которая инициализирует переменные структуры. Возьмём очень простой пример.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. struct stDemo 05. { 06. private: 07. int value; 08. //+----------------+ 09. void Message_0(void) 10. { 11. Print(__FUNCTION__); 12. } 13. //+----------------+ 14. void Message_1(void) 15. { 16. Print(__FUNCTION__); 17. } 18. //+----------------+ 19. public : 20. //+----------------+ 21. void Init(void) 22. { 23. value = 1; 24. } 25. //+----------------+ 26. void Check(void) 27. { 28. switch (value) 29. { 30. case 1: 31. Message_0(); 32. break; 33. case 2: 34. Message_1(); 35. break; 36. default: 37. Print("Unknown [ ", value, " ]..."); 38. }; 39. } 40. //+----------------+ 41. }; 42. //+------------------------------------------------------------------+ 43. void OnStart(void) 44. { 45. stDemo demo; 46. 47. demo.Init(); 48. demo.Check(); 49. } 50. //+------------------------------------------------------------------+
Код 01
Код 01 не требует подробного объяснения, поскольку в нём используются понятия, рассмотренные в других статьях. Его цель — показать, что происходит, если не инициализировать переменные структуры.
Компиляция кода 01 без изменений даёт в MetaTrader 5 результат, показанный ниже.

Изображение 01
Одно-единственное изменение приводит к другому результату.
. . . 42. //+------------------------------------------------------------------+ 43. void OnStart(void) 44. { 45. stDemo demo; 46. 47. // demo.Init(); 48. demo.Check(); 49. } 50. //+------------------------------------------------------------------+
Фрагмент 01
Во фрагменте 01 мы закомментировали инструкцию, соответствующую строке 47 кода 01. В результате скомпилированная программа ведёт себя иначе. Это изменение показывает последствия пропуска инициализации при написании полностью структурированного кода. После повторной компиляции код 01 даёт следующий результат:

Изображение 02
Посмотрите, что происходит, мой уважаемый читатель. Когда мы объявляем структуру в строке 45, компилятор автоматически выделяет достаточно памяти для содержащихся в ней переменных и инициализирует эту область нулями. Хотя можно было бы ожидать одинакового поведения во всех случаях, каждый язык программирования управляет памятью по-своему. MQL5 инициализирует эту область, тогда как в других языках память может сохранять остаточное содержимое.
Например, в устаревшем коде, написанном на C, компилятор лишь указывает операционной системе, сколько памяти нужно выделить. Данная область не очищается, поэтому в ней могут сохраняться остаточные байты от предыдущих операций. Эту особенность использовали для обхода средств безопасности операционной системы, хотя этот вопрос выходит за рамки данной статьи.
С появлением объектно-ориентированного программирования такие ошибки стали встречаться реже. Класс может объявить конструктор — специальный метод, который MQL5 автоматически выполняет при создании экземпляра и который позволяет инициализировать состояние объекта. Этот механизм снижает риск пропустить инициализацию.
Многие, особенно новички, считают, что классы никак не связаны со структурами. Они считают, что изучать структуры — пустая трата времени, и предпочитают сразу переходить к классам. Это ошибка: класс — особая структура. Понимание структур и умение писать код в стиле структурного программирования значительно облегчают понимание классов. Именно поэтому я затронул эту тему ранее: это помогло вам познакомиться с основой, которую классы лишь расширяют.
Следует уточнить один момент. Хотя MQL5 использует модель объектно-ориентированного программирования, близкую к C++, эти языки не тождественны. MQL5 поддерживает инкапсуляцию с помощью модификаторов public, protected и private, наследование, полиморфизм, перегрузку методов, виртуальные функции, статические члены, шаблоны и абстрактные классы. Понимайте каждый механизм в соответствии с синтаксисом и правилами MQL5, а не как полное воспроизведение C++. Эта статья ограничивается конструкторами и деструкторами; наследование, управление доступом и полиморфизм будут рассмотрены позже.
Если позже вы будете работать с C++, не считайте, что его правила совпадают с правилами MQL5. Знания, полученные здесь, служат концептуальной основой, однако создание объектов, наследование, виртуальные функции и управление ресурсами следует изучать с учётом конкретных правил каждого языка.
Вернёмся к главной мысли. Чтобы упростить код 01 и не забыть инициализировать переменные структуры, внесём следующие простые изменения.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. class stDemo 05. { 06. private: 07. int value; 08. //+----------------+ 09. void Message_0(void) 10. { 11. Print(__FUNCTION__); 12. } 13. //+----------------+ 14. void Message_1(void) 15. { 16. Print(__FUNCTION__); 17. } 18. //+----------------+ 19. public : 20. //+----------------+ 21. stDemo() 22. { 23. value = 1; 24. } 25. //+----------------+ 26. void Check(void) 27. { 28. switch (value) 29. { 30. case 1: 31. Message_0(); 32. break; 33. case 2: 34. Message_1(); 35. break; 36. default: 37. Print("Unknown [ ", value, " ]..."); 38. }; 39. } 40. //+----------------+ 41. }; 42. //+------------------------------------------------------------------+ 43. void OnStart(void) 44. { 45. stDemo demo; 46. 47. demo.Check(); 48. } 49. //+------------------------------------------------------------------+
Код 02
Код 02 намеренно похож на код 01. Это сходство показывает, как объектно-ориентированное программирование упрощает структурный код и превращает его в объектно-ориентированный. Чтобы упростить объяснение, пока я буду называть его структурным кодом. Внимательно рассмотрим изменения.
Сначала в четвёртой строке мы заменяем ключевое слово struct на class. Начиная с этого момента компилятор немного иначе интерпретирует структурный код из кода 01. Класс может объявлять один или несколько конструкторов и только один деструктор. Оба являются специальными методами, связанными с жизненным циклом объекта.
MQL5 автоматически выполняет конструктор при создании экземпляра. Конструктор инициализирует члены и устанавливает объект в корректное состояние. Если класс не объявляет ни одного конструктора, компилятор предоставляет конструктор по умолчанию. MQL5 автоматически выполняет деструктор, когда завершается жизненный цикл экземпляра. Его функция сводится к освобождению ресурсов, которые объект приобрёл и которые требуют явного освобождения. Затем среда завершает деинициализацию членов и при необходимости управляет размещением объекта в памяти. Хотя всё это кажется простым, необходимо хорошо понимать обе фазы жизненного цикла объекта.
Поскольку код 02 относительно прост, мы явно объявляем только конструктор. Обратите внимание на важную деталь: класс может иметь несколько конструкторов, но только один деструктор; этот аспект мы рассмотрим позже. Конструктор в строке 21 кода 02 выполняет ту же функцию, что и строка 21 кода 01, хотя его объявление отличается. В коде 01 мы объявляем подпрограмму Init, которая инициализирует внутреннюю переменную структуры. Строка 47 вызывает эту подпрограмму. Если бы мы опустили этот вызов, то получили бы результат, отличный от ожидаемого.
При компиляции строки 45 компилятор определяет, какой конструктор соответствует данному способу создания объекта. Если ни один из них не подходит, компиляция завершается ошибкой. Когда программа создаёт экземпляр, MQL5 выполняет выбранный конструктор. В коде 02 конструктор объявлен явно; если класс не объявляет ни одного конструктора, компилятор предоставляет конструктор по умолчанию.
Другими словами, при создании объекта в строке 45 кода 02 выполняется конструктор, определённый в строке 21. Конструктор — это особый метод: он имеет то же имя, что и класс, может принимать параметры и выполнять логику инициализации, но не объявляет тип возврата и не может возвращать значения. Функция с другим именем — это обычный метод.
Создание экземпляра в строке 45 приводит к выполнению конструктора, инициализации членов объекта и устраняет необходимость вызывать подпрограмму Init из кода 01. Код 02 даёт результат, показанный на рисунке 01.
Интересно, не правда ли, уважаемый читатель? Благодаря всего одному изменению код стал гораздо надёжнее и проще. Но мы лишь в самом начале. Что произошло бы, если бы мы явно не реализовали конструктор в коде 02? Компилятор сгенерировал бы немного другой код. Код 03 позволяет это проверить и отличается от кода 02 лишь одной деталью.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. class stDemo 05. { 06. private: 07. int value; 08. //+----------------+ 09. void Message_0(void) 10. { 11. Print(__FUNCTION__); 12. } 13. //+----------------+ 14. void Message_1(void) 15. { 16. Print(__FUNCTION__); 17. } 18. //+----------------+ 19. public : 20. //+----------------+ 21. void Check(void) 22. { 23. switch (value) 24. { 25. case 1: 26. Message_0(); 27. break; 28. case 2: 29. Message_1(); 30. break; 31. default: 32. Print("Unknown [ ", value, " ]..."); 33. }; 34. } 35. //+----------------+ 36. }; 37. //+------------------------------------------------------------------+ 38. void OnStart(void) 39. { 40. stDemo demo; 41. 42. demo.Check(); 43. } 44. //+------------------------------------------------------------------+
Код 03
Обратите внимание, что в коде 03 конструктор УЖЕ НЕ УКАЗЫВАЕТСЯ явно. Это не означает, что у объекта нет конструктора: компилятор предоставляет конструктор по умолчанию. Поэтому выполнение кода 03 даёт тот же результат, что и на рисунке 02.
Ну что ж, это выглядит запутанно, но, кажется, я начинаю понимать логику классов. Поправь меня, если я ошибаюсь. Объявляя структуру данных, мы создаём способ представить конкретную запись. Когда мы её используем, нам часто приходится вызывать подпрограмму, которая инициализирует её внутренние переменные, чтобы избежать остаточных данных или неинициализированных состояний. Верно? Верно, мой уважаемый читатель. Тогда я понимаю идею. Поскольку мы можем забыть о таком вызове, мы заменяем подпрограмму инициализации конструктором и тем самым не допускаем, чтобы переменные структуры оставались неинициализированными. Верно ли такое понимание?
Более или менее. Конструктор может принимать параметры и выполнять всю логику, необходимую для инициализации объекта. Существенное отличие от функции заключается в том, что конструктор НЕ ОБЪЯВЛЯЕТ ТИП ВОЗВРАТА И НЕ МОЖЕТ ВОЗВРАЩАТЬ значение коду, создающему экземпляр. Таким образом, мы можем перенести логику инициализации в конструктор, но при этом должны сохранить в объекте любой результат, который раньше возвращала функция, либо вычислить его с помощью отдельной функции. При создании объекта MQL5 автоматически выполняет эту логику. Идея перенести инициализацию в конструктор верна.
Интересно. Объектно-ориентированное программирование выглядит очень привлекательно. Многим программистам оно нравится, поскольку позволяет лучше контролировать поведение кода. Но теперь у меня возникает один вопрос. Мы используем код 01, чтобы научиться работать с этой парадигмой, хотя он не позволяет вызывающему коду передавать аргумент инициализации переменной. Предположим, что строка 47 кода 01 принимала бы параметр для инициализации. Как это изменение повлияет на код 02, в котором уже используется объектно-ориентированное программирование?
Это хороший вопрос, уважаемый читатель. Чтобы нагляднее представить этот случай и объяснить, как вызывающая сторона передаёт аргументы подпрограмме инициализации, мы воспользуемся кодом 04, приведённым ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. struct stDemo 05. { 06. private: 07. int value; 08. //+----------------+ 09. void Message_0(void) 10. { 11. Print(__FUNCTION__); 12. } 13. //+----------------+ 14. void Message_1(void) 15. { 16. Print(__FUNCTION__); 17. } 18. //+----------------+ 19. public : 20. //+----------------+ 21. void Init(int arg) 22. { 23. value = 1; 24. Check(__FUNCTION__); 25. value = arg; 26. } 27. //+----------------+ 28. void Check(string arg) 29. { 30. Print("Call coming from: ", arg); 31. switch (value) 32. { 33. case 1: 34. Message_0(); 35. break; 36. case 2: 37. Message_1(); 38. break; 39. default: 40. Print("Unknown [ ", value, " ]..."); 41. }; 42. } 43. //+----------------+ 44. }; 45. //+------------------------------------------------------------------+ 46. void OnStart(void) 47. { 48. stDemo demo; 49. 50. demo.Init(2); 51. demo.Check(__FUNCTION__); 52. } 53. //+------------------------------------------------------------------+
Код 04
Как и предыдущие, этот код очень прост и не требует подробных пояснений. Его выполнение приводит к результату, показанному ниже.

Изображение 03
На изображении 03 выделенные точки указывают на вызывающий код по строке, из которой вызывается подпрограмма, поскольку строка 30 выводит строку формата, полученную в качестве аргумента. В строке 21 подпрограмма инициализации требует аргумент, чтобы присвоить его переменной структуры. Поскольку для этого параметра не задано значение по умолчанию (default), вызывающий код должен передать этот аргумент при вызове подпрограммы. В коде 04 код в строке 50 выступает как вызывающий код и передаёт аргумент подпрограмме инициализации.
Теперь мы подошли к той части, которая многих сбивает с толку: как преобразовать код 04 так, чтобы в нём использовалось объектно-ориентированное программирование? Есть два варианта. Подпрограмма инициализации в коде 04 не возвращает никакого значения, поэтому мы можем перенести её логику в конструктор. Если бы ей нужно было возвращать результат, нам пришлось бы оставить эту часть в виде функции или передавать результат с помощью другого механизма.
Поняв, как это работает, мы преобразуем код 04 так же, как и предыдущие. Есть два немного различающихся метода. Начнём с первого.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. struct stDemo 05. { 06. private: 07. int value; 08. //+----------------+ 09. void Message_0(void) 10. { 11. Print(__FUNCTION__); 12. } 13. //+----------------+ 14. void Message_1(void) 15. { 16. Print(__FUNCTION__); 17. } 18. //+----------------+ 19. public : 20. //+----------------+ 21. stDemo(int arg) 22. { 23. value = 1; 24. Check(__FUNCTION__); 25. value = arg; 26. } 27. //+----------------+ 28. void Check(string arg) 29. { 30. Print("Call coming from: ", arg); 31. switch (value) 32. { 33. case 1: 34. Message_0(); 35. break; 36. case 2: 37. Message_1(); 38. break; 39. default: 40. Print("Unknown [ ", value, " ]..."); 41. }; 42. } 43. //+----------------+ 44. }; 45. //+------------------------------------------------------------------+ 46. void OnStart(void) 47. { 48. stDemo demo(2); 49. 50. demo.Check(__FUNCTION__); 51. } 52. //+------------------------------------------------------------------+
Код 05
С этого момента легко запутаться, поэтому стоит быть внимательным. Точно так же, как мы преобразовали код 01 в код 02, теперь мы преобразуем код 04 в код 05. Однако строка 48 здесь является решающей. Инструкция в этой строке определяет вывод программы.
Вернёмся к сути. Если вы не поняли предыдущее объяснение, ещё раз разберитесь в работе конструктора. Понять это необходимо, чтобы разобраться в коде 05.
После этого изменения код 05 даёт результат, показанный ниже.

Изображение 04
Разница между изображениями 03 и 04 заключается в вызывающем коде при первом вызове, указанном в первой строке обоих изображений. Это совпадение подтверждает, что оба фрагмента кода совместимы и работают одинаково. Следовательно, код легко понять.
Помните, я упоминал о двух способах получить один и тот же результат в конструкторе? Во втором варианте используется фрагмент 02; поскольку остальная часть кода не меняется, мы изменим только эти строки.
. . . 20. //+----------------+ 21. stDemo(int arg) 22. :value(1) 23. { 24. Check(__FUNCTION__); 25. value = arg; 26. } 27. //+----------------+ . . .
Фрагмент 02
Фрагмент 02 заменяет соответствующие строки кода 05. Список инициализации инициализирует член до выполнения тела конструктора. Однако результат совпадает с результатом на изображении 04.
Но зачем использовать объявление из фрагмента 02? Оно кажется более запутанным. Фрагмент 02 позволяет инициализировать члены до выполнения тела конструктора. Более глубокий анализ классов позволит понять, почему иногда целесообразно инициализировать члены именно таким образом. Пока достаточно знать, что такое объявление допустимо и что компилятор может без проблем его интерпретировать.
Заключительные замечания
В этой статье мы рассмотрели, что такое классы и почему они появились. Хотя эта статья лишь вводит в тему, уже из предыдущих статей видно, что классы значительно расширяют возможности кода и дают преимущества по сравнению с простыми структурами. Небольшое изменение превращает код в стиле структурного программирования в объектно-ориентированный код и даёт упомянутые преимущества.
Хотя многие представляют объектно-ориентированное программирование как трудную и сложную парадигму, эта статья показывает, что оно может быть вполне доступным. Напротив, если вы уже владеете структурным программированием, понять эту новую парадигму будет гораздо проще.
Это введение ещё не охватывает всех необходимых механизмов; поэтому мы пока не можем вернуться к теме очередей, списков и деревьев. В следующей статье мы подробнее рассмотрим классы и объясним, как объявлять деструкторы, чтобы правильно освобождать ресурсы, приобретённые каждым объектом. Встретимся позже!
| Файл | Описание |
|---|---|
| Код 01 | Демонстрационный файл |
| Код 02 | Демонстрационный файл |
| Код 03 | Демонстрационный файл |
| Код 04 | Демонстрационный файл |
| Код 05 | Демонстрационный файл |
Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/16750
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Автоматизация торговых стратегий в MQL5 (Часть 49): Разворотный паттерн "Квазимодо" (QM)
Моделирование рынка: Position View (XIII)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования