Del básico al intermedio: Clases (I)
Introducción
En el artículo anterior, Del básico al intermedio: Colas, listas y árboles (V), estudiamos cómo implementar una estructura de árbol. Sin embargo, antes de retomar ese tema debemos abordar una cuestión que he pospuesto hasta ahora. Ya no podemos aplazarla: sin explicar cómo funciona la programación orientada a objetos, resulta difícil presentar las estructuras y los componentes necesarios. Aunque en los últimos artículos ya he utilizado recursos de este paradigma sin explicarlos, el contenido anterior podía entenderse sin entrar en demasiados detalles.
Los próximos artículos exigirán una comprensión más profunda de este paradigma. Si entendiste bien el artículo anterior, habrás notado que MetaTrader 5 muestra constantemente alertas sobre instancias no destruidas o memoria no liberada. Aunque resultan molestas y debemos ajustar el código para eliminarlas, esa corrección exige comprender cómo funciona la programación orientada a objetos. Por eso, apartamos temporalmente ese tema para explicar al menos sus fundamentos.
Sin más preámbulos, centrémonos en el tema principal.
Clases (I)
Como indica el título, aquí estudiamos las clases, un concepto fundamental de la programación orientada a objetos. En esta primera aproximación abordamos solo lo necesario para seguir los próximos artículos. Por ahora, no profundizamos en todos los aspectos de las clases, pues los artículos sobre programación estructurada ya explicaron algunos. Consulta los artículos anteriores para obtener más detalles.
No obstante, debemos explicar correctamente algunas características y posibilidades de las clases. Comprender estos principios te permitirá entender por qué funcionan los códigos presentados más adelante y cómo aprovecharlos. Busco que puedas seguirlos con pocas explicaciones por mi parte, porque reduciré progresivamente los comentarios.
Empecemos por la base: para entender las clases, primero hay que comprender otro concepto explicado en artículos anteriores, el de estructura.
En el artículo Del básico al intermedio: Struct (I), presentamos las estructuras de datos. Las estructuras permiten reunir variables relacionadas bajo un tipo definido por el programador. Otros artículos también explicaron su uso, pero este inauguró la serie dedicada exclusivamente al tema. El objetivo era introducirte en la programación estructurada.
Estudiar primero la programación estructurada facilita mucho la comprensión de las clases. Cuando surgieron las clases, el estudio exhaustivo de la programación estructurada ya había revelado qué aspectos debían mejorarse. La programación orientada a objetos surgió como una evolución natural. Por eso, si aún no has asimilado bien el concepto de estructura, te sugiero que repases los artículos anteriores. La explicación parte de esos conocimientos.
En varios artículos sobre estructuras demostré que podíamos crear código estructurado y expliqué el origen de esa denominación. Sin embargo, si estudiaste el tema con calma y escribiste código estructurado, quizá hayas detectado un pequeño inconveniente. Este modelo facilita el desarrollo de código mucho más complejo que la programación heredada, en la que no existe el concepto de estructura de datos. La estructura mantiene una limitación concreta: sus variables no siempre se inicializan correctamente.
Espera un momento. No entiendo. ¿Qué dificultad presenta inicializar las variables de la estructura? Basta con declarar una rutina y llamarla antes de utilizar esas variables. No veo ninguna dificultad. A primera vista, tienes razón, mi querido lector. Esa solución es válida, y tu observación demuestra que estudiaste y comprendiste los artículos anteriores.
Sin embargo, la rutina de inicialización puede no ejecutarse. Estos fallos son poco frecuentes, pero dificultan el desarrollo. Con mucha más frecuencia, olvidamos llamar a la rutina que inicializa las variables. En esos casos, la estructura pierde fiabilidad y necesita alguna mejora.
Analicemos el error más común: no llamar a la rutina que inicializa las variables de la estructura. Utilizaremos un ejemplo muy sencillo.
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. //+------------------------------------------------------------------+
Código 01
El código 01 no requiere una explicación detallada, pues utiliza conceptos tratados en otros artículos. Su propósito es mostrar qué ocurre cuando no inicializamos las variables de una estructura.
La compilación del código 01 sin modificaciones genera en MetaTrader 5 el resultado que aparece a continuación.

Imagen 01
Una sola modificación produce un resultado diferente.
. . . 42. //+------------------------------------------------------------------+ 43. void OnStart(void) 44. { 45. stDemo demo; 46. 47. // demo.Init(); 48. demo.Check(); 49. } 50. //+------------------------------------------------------------------+
Fragmento 01
En el Fragmento 01 comentamos la instrucción equivalente a la línea 47 del código 01. Como consecuencia, el programa compilado se comporta de manera distinta. El cambio muestra las consecuencias de omitir la inicialización al escribir código completamente estructurado. La nueva compilación del código 01 genera el resultado que aparece a continuación.

Imagen 02
Observa qué ocurre, mi querido lector. Cuando declaramos la estructura en la línea 45, el compilador reserva automáticamente memoria suficiente para las variables que contiene e inicializa esa región con ceros. Aunque podrías esperar el mismo comportamiento en todos los casos, cada lenguaje gestiona la memoria de forma diferente. MQL5 inicializa esta región; en otros lenguajes, la memoria puede conservar contenido residual.
Por ejemplo, en el código heredado escrito en C, el compilador solo indica al sistema operativo cuánta memoria debe reservar. Esa región no se limpia, por lo que puede conservar bytes residuales de operaciones anteriores. Esta característica se aprovechaba para eludir la seguridad del sistema operativo, aunque ese tema queda fuera de este artículo.
Con la aparición de la programación orientada a objetos, estos errores se volvieron menos frecuentes. Una clase puede declarar un constructor, un método especial que MQL5 ejecuta automáticamente al crear una instancia y que permite inicializar el estado del objeto. Este mecanismo reduce el riesgo de omitir la inicialización.
Muchas personas, sobre todo quienes empiezan, creen que las clases no tienen relación con las estructuras. Consideran que estudiar estas últimas es una pérdida de tiempo y prefieren pasar directamente a las clases. Es un error: una clase es una estructura especial. Entender las estructuras y aprender a escribir código estructurado facilita mucho la comprensión de las clases. Por eso abordé aquel tema con anterioridad: ese estudio te permitió familiarizarte con una base que las clases solo amplían.
Conviene precisar un punto. Aunque MQL5 utiliza un modelo de programación orientada a objetos próximo al de C++, ambos lenguajes no son equivalentes. MQL5 admite encapsulación mediante los modificadores public, protected y private, herencia, polimorfismo, sobrecarga de métodos, funciones virtuales, miembros estáticos, plantillas y clases abstractas. Interpreta cada mecanismo conforme a la sintaxis y las reglas de MQL5, no como una reproducción completa de C++. Este artículo se limita a los constructores y destructores; la herencia, el control de acceso y el polimorfismo se abordarán más adelante.
Si más adelante trabajas con C++, no des por sentado que sus reglas son idénticas a las de MQL5. Los conocimientos adquiridos aquí sirven como base conceptual, pero estudia la creación de objetos, la herencia, las funciones virtuales y la gestión de recursos según las reglas específicas de cada lenguaje.
Retomemos el punto principal. Para simplificar el código 01 y evitar que olvidemos inicializar las variables de la estructura,introducimos las sencillas modificaciones siguientes.
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. //+------------------------------------------------------------------+
Código 02
El código 02 se parece deliberadamente al código 01. La semejanza muestra cómo la programación orientada a objetos simplifica un código estructurado y lo convierte en código orientado a objetos. Para simplificar la explicación, por ahora lo llamaré código estructurado. Observa atentamente los cambios.
Primero, en la línea cuatro, sustituimos la palabra reservada struct por class. A partir de ahí, el compilador interpreta de manera ligeramente distinta el código estructurado del código 01. Una clase puede declarar uno o varios constructores y un único destructor. Ambos son métodos especiales vinculados al ciclo de vida del objeto.
MQL5 ejecuta automáticamente el constructor al crear una instancia. El constructor inicializa los miembros y establece un estado válido para el objeto. Si la clase no declara ningún constructor, el compilador proporciona un constructor predeterminado. MQL5 ejecuta automáticamente el destructor cuando finaliza la vida de la instancia. Su función se limita a liberar los recursos que ese objeto haya adquirido y que requieran liberación explícita. Después, el entorno completa la desinicialización de los miembros y gestiona el almacenamiento del objeto según corresponda. Aunque el funcionamiento parece sencillo, hay que comprender bien ambas fases del ciclo de vida del objeto.
Como el código 02 es relativamente sencillo, solo declaramos explícitamente el constructor. Observa un detalle importante: una clase puede tener varios constructores, pero un único destructor, aspecto que analizaremos más adelante. El constructor de la línea 21 del código 02 cumple la misma función que la línea 21 del código 01, aunque su declaración es distinta. En el código 01, declaramos la rutina Init, que inicializa la variable interna de la estructura. La línea 47 llama a esa rutina. Si omitíamos esa llamada, obteníamos un resultado distinto del previsto.
Al compilar la línea 45, el compilador determina qué constructor es compatible con la forma de creación del objeto. Si no existe ninguno compatible, la compilación falla. Cuando el programa crea la instancia, MQL5 ejecuta el constructor seleccionado. El código 02 lo declara explícitamente; si una clase no declara ningún constructor, el compilador proporciona uno predeterminado.
En otras palabras, la creación del objeto en la línea 45 del código 02 ejecuta el constructor definido en la línea 21. El constructor es un método especial: tiene el mismo nombre que la clase, puede recibir parámetros y ejecutar lógica de inicialización, pero no declara retorno ni puede devolver valores. Una función con otro nombre es un método ordinario.
La creación de la instancia en la línea 45 ejecuta el constructor, inicializa los miembros del objeto y elimina la necesidad de llamar a la rutina Init del código 01.El código 02 produce el resultado de la imagen 01.
Interesante, ¿verdad, mi querido lector? Con un solo cambio, el código es ahora mucho más fiable y sencillo. Pero apenas hemos comenzado. ¿Qué ocurriría si no implementáramos explícitamente el constructor del código 02? El compilador generaría un código ligeramente distinto. El código 03 permite comprobarlo y solo difiere del código 02 en un detalle.
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. //+------------------------------------------------------------------+
Código 03
Observa que el código 03 YA NO DECLARA explícitamente el constructor. Eso no significa que el objeto carezca de constructor: el compilador proporciona uno predeterminado. Por eso, la ejecución del código 03 genera la misma salida que la imagen 02.
Vaya, parece confuso, pero creo que empiezo a entender la lógica de las clases. Corrígeme si me equivoco. Al declarar una estructura de datos, creamos una forma de representar un registro concreto. Cuando la utilizamos, muchas veces debemos llamar a una rutina que inicialice sus variables internas para evitar contenido residual o estados no inicializados. ¿Correcto? Correcto, mi querido lector. Entonces, entiendo la idea. Como podríamos olvidar esa llamada, sustituimos la rutina de inicialización por un constructor y evitamos que las variables de la estructura queden sin inicializar.¿Es correcta esa interpretación?
Más o menos. Un constructor puede recibir parámetros y ejecutar toda la lógica necesaria para inicializar el objeto. La diferencia relevante frente a una función radica en que el constructor NO DECLARA RETORNO NI PUEDE DEVOLVER un valor al código que crea la instancia. Por tanto, podemos trasladar la lógica de inicialización al constructor, pero debemos almacenar en el objeto cualquier resultado que antes devolviera una función o calcularlo mediante una función aparte. Al crear el objeto, MQL5 ejecuta automáticamente esa lógica. La idea de trasladar la inicialización al constructor es correcta.
Interesante. La programación orientada a objetos parece muy atractiva. A muchos programadores les gusta porque ofrece un mayor control sobre el comportamiento del código. Pero ahora me surge una duda. Utilizamos el código 01 para aprender a trabajar con este paradigma, aunque no permite que el autor de llamada proporcione el argumento de inicialización de la variable. Supongamos que la línea 47 del código 01 aceptara un parámetro para la inicialización. ¿Cómo afectaría ese cambio al código 02, que ya utiliza programación orientada a objetos?
Es una buena pregunta, mi querido lector. Para representar ese caso con mayor claridad y explicar cómo el autor de llamada transmite argumentos a la rutina de inicialización, utilizaremos el código 04, presentado a continuación.
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. //+------------------------------------------------------------------+
Código 04
Al igual que los anteriores, este código es muy básico y no requiere una explicación detallada. Su ejecución genera el resultado que aparece a continuación.

Imagen 03
En la imagen 03, los puntos destacados identifican al autor de llamada mediante la línea desde la que invoca la rutina, porque la línea 30 imprime la cadena de formato recibida como argumento. En la línea 21, la rutina de inicialización exige un argumento para asignarlo a la variable de la estructura. Como el parámetro no define un valor predeterminado (default), el autor de llamada debe proporcionar ese argumento al invocar la rutina. En el código 04, el código de la línea 50 actúa como autor de llamada y transmite el argumento a la rutina de inicialización.
Ahora llegamos a la parte que confunde a muchas personas: ¿cómo transformar el código 04 para que utilice programación orientada a objetos? Hay dos posibilidades. La rutina de inicialización del código 04 no devuelve ningún valor; por ello, podemos trasladar su lógica al constructor. Si necesitara devolver un resultado, deberíamos mantener esa parte en una función o comunicar el resultado mediante otro mecanismo.
Una vez comprendido el funcionamiento, transformamos el código 04 igual que los anteriores. Existen dos métodos ligeramente distintos. Empecemos por el primero.
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. //+------------------------------------------------------------------+
Código 05
A partir de aquí es fácil confundirse, por lo que conviene prestar atención. Del mismo modo que transformamos el código 01 en el código 02, ahora convertimos el código 04 en el código 05. Sin embargo, la línea 48 es decisiva. La instrucción de esa línea determina la salida del programa.
Volvamos al punto esencial. Si no entendiste la explicación anterior, repasa el funcionamiento del constructor. Comprenderlo es imprescindible para entender el código 05.
Con este cambio, el código 05 produce el resultado que aparece a continuación.

Imagen 04
La diferencia entre las imágenes 03 y 04 está en el autor de llamada de la primera invocación, identificado en la primera línea de ambas. La coincidencia confirma que los dos códigos son compatibles y funcionan de la misma manera. Por tanto, el código resulta fácil de interpretar.
¿Recuerdas que mencioné dos formas de obtener el mismo resultado en el constructor? La segunda utiliza el fragmento 02; como el resto del código no cambia, solo modificaremos esas líneas.
. . . 20. //+----------------+ 21. stDemo(int arg) 22. :value(1) 23. { 24. Check(__FUNCTION__); 25. value = arg; 26. } 27. //+----------------+ . . .
Fragmento 02
El fragmento 02 sustituye las líneas correspondientes del código 05. La lista de inicialización inicializa el miembro antes de ejecutar el cuerpo del constructor. El resultado, sin embargo, coincide con el de la imagen 04.
Pero ¿por qué usaríamos la declaración del fragmento 02? Parece más confusa. El fragmento 02 permite inicializar los miembros antes de ejecutar el cuerpo del constructor. Un análisis más profundo de las clases permitirá entender por qué a veces conviene inicializar los miembros de ese modo. Por ahora basta con saber que esta declaración es válida y que el compilador puede interpretarla sin problemas.
Consideraciones finales
En este artículo hemos visto qué son las clases y por qué surgieron. Aunque el artículo solo introduce el tema, los artículos anteriores ya permiten comprobar que las clases amplían considerablemente las posibilidades del código y ofrecen ventajas frente a las estructuras simples. Una ligera modificación convierte el código estructurado en código orientado a objetos y aporta las ventajas mencionadas.
Aunque muchos presentan la programación orientada a objetos como un paradigma difícil y complejo, este artículo demuestra que puede resultar accesible. Por el contrario, si ya dominas la programación estructurada, comprender este nuevo paradigma resulta mucho más sencillo.
Esta introducción aún no cubre todos los mecanismos necesarios; por ello, todavía no podemos retomar el tema de las colas, listas y árboles. El próximo artículo profundiza en las clases y explica cómo declarar destructores para liberar correctamente los recursos adquiridos por cada objeto. Nos vemos entonces.
| Archivo | Descripción |
|---|---|
| Código 01 | Archivo de demostración |
| Código 02 | Archivo de demostración |
| Código 03 | Archivo de demostración |
| Código 04 | Archivo de demostración |
| Código 05 | Archivo de demostración |
Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/16750
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Simulación de mercado: Position View (XIV)
Simulación de mercado: Position View (XIII)
Particularidades del trabajo con números del tipo double en MQL4
Características del Wizard MQL5 que debe conocer (Parte 70): Uso de patrones del SAR y el RVI con una red de kernel exponencial
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso