English Русский 中文 Español Deutsch 日本語
preview
Aprendendo MQL5 do iniciante ao profissional (Parte III): Tipos de dados complexos e arquivos inclusos

Aprendendo MQL5 do iniciante ao profissional (Parte III): Tipos de dados complexos e arquivos inclusos

MetaTrader 5Exemplos |
300 5
Oleh Fedorov
Oleh Fedorov

Introdução

Este artigo é uma continuação da série para iniciantes. Presumo que o leitor já tenha entendido o conteúdo dos dois artigos anteriores.

O primeiro artigo é uma introdução. Ele assume que o leitor não possui experiência prévia em programação e permite familiarizar-se com as ferramentas necessárias para o trabalho do programador, descreve os principais tipos de programas e introduz alguns conceitos básicos, como o conceito de "função".

O segundo artigo descreve o trabalho com dados. Ele introduz os conceitos de "literal", "variável", "tipo de dado", "operador", etc., e examina os principais operadores para manipulação de dados: aritméticos, lógicos, bit a bit...

Neste artigo, descreverei como o programador pode criar tipos de dados complexos, como:

  • estruturas;
  • uniões (union);
  • classes (em nível introdutório);
  • tipos que permitem usar o nome de uma variável como função. Isso permite, entre outras coisas, passar funções como parâmetros para outras funções.

Além disso, o artigo explica como incluir arquivos de texto externos usando a diretiva de pré-processador #include, para proporcionar modularidade e flexibilidade ao programa. Lembre-se de que os dados podem ser organizados de várias maneiras, mas o compilador sempre precisa saber quanta memória será necessária para o programa, e, por isso, antes de usar os dados, é necessário descrevê-los especificando o seu tipo.

Tipos de dados simples, como double, enum, string e outros, foram descritos no segundo artigo. Lá foram examinadas detalhadamente tanto as variáveis (dados que mudam durante a execução) quanto as constantes. No entanto, na programação, muitas vezes surgem situações em que é mais conveniente compor tipos mais complexos a partir de dados simples. É exatamente sobre essas "construções" que falaremos na primeira parte deste artigo.

Quanto mais modular for a estrutura de um programa, mais fácil será desenvolvê-lo e mantê-lo. Isso é especialmente importante ao trabalhar em equipe. No entanto, até mesmo programadores "solitários" acham muito mais fácil procurar erros em pequenos trechos de código do que em um bloco único e contínuo. Isso é ainda mais relevante se você voltar ao código após um longo período, para adicionar recursos ao programa ou corrigir erros lógicos que inicialmente passaram despercebidos.

Se você definir estruturas de dados adequadas, criar funções convenientes em vez de um fluxo contínuo de condições e loops, e distribuir diferentes blocos de código logicamente relacionados em arquivos distintos, as alterações serão muito mais fáceis de implementar.


Estruturas

A estrutura descreve um conjunto complexo de dados que é conveniente armazenar em uma única variável. Por exemplo, informações sobre o horário de uma negociação intradiária devem conter minutos, segundos e horas.

Claro, é possível criar três variáveis para cada componente e acessá-las conforme necessário. No entanto, como esses dados fazem parte de uma única descrição e, geralmente, são usados em conjunto, é mais conveniente criar um tipo separado para esses dados. Além disso, a estrutura pode conter dados adicionais de outros tipos, como o fuso horário ou qualquer outra informação necessária ao programador.

No caso mais simples, a estrutura é descrita da seguinte forma:

struct IntradayTime {
  int hours;
  int minutes;
  int seconds;
  string timeCodeString;
};  // note the semicolon after the curly brace

Exemplo 1. Exemplo de estrutura para descrever o horário de uma negociação.

Com este código, criamos um novo tipo de dado, IntradayTime. Dentro das chaves {} dessa descrição, estão listadas todas as variáveis que desejamos agrupar. Assim, todas as variáveis do tipo IntradayTime conterão horas, minutos e segundos.

O acesso a cada parte da estrutura em cada variável pode ser feito com o operador de ponto ".".

IntradayTime dealEnterTime;

dealEnterTime.hours = 8;
dealEnterTime.minutes = 15;
dealEnterTime.timeCodeString = "GMT+2";

Exemplo 2. Uso de variáveis do tipo estrutura.

Ao descrever uma estrutura, suas variáveis "internas" (geralmente chamadas de "campos") podem ter qualquer tipo de dado permitido, incluindo outras estruturas. Por exemplo:

// Nested structure
struct TradeParameters
{
   double stopLoss;
   double takeProfit;
   int magicNumber;
};

// Main structure
struct TradeSignal
{
   string          symbol;    // Symbol name
   ENUM_ORDER_TYPE orderType; // Order type (BUY or SELL)
   double          volume;    // Order volume
   TradeParameters params;    // Nested structure as parameter type
};

// Using the structure
void OnStart()
{

// Variable description for the structure
   TradeSignal signal;

// Initializing structure fields
   signal.symbol = Symbol();
   signal.orderType = ORDER_TYPE_BUY;
   signal.volume = 0.1;

   signal.params.stopLoss = 20;
   signal.params.takeProfit = 40;
   signal.params.magicNumber = 12345;

// Using data in an expression
   Print("Symbol: ",  signal.symbol);
   Print("Order type: ",  signal.orderType);
   Print("Volume: ",  signal.volume);
   Print("Stop Loss: ",  signal.params.stopLoss);
   Print("Take Profit: ",  signal.params.takeProfit);
   Print("Magic Number: ",  signal.params.magicNumber);
}

Exemplo 3. Uso de uma estrutura para descrever o tipo dos campos de outra estrutura.


Se os valores iniciais da estrutura forem constantes e não expressões, é possível usar uma sintaxe reduzida para inicialização. Para isso, utilizam-se chaves {}. Por exemplo, o bloco de inicialização do exemplo anterior poderia ser reescrito assim:

TradeSignal signal = 
  {
    "EURUSD", 
    ORDER_TYPE_BUY, 
    0.1, 
 
     {20.0,  40.0,  12345}
  };

Exemplo 4. Inicialização de uma estrutura utilizando constantes.


A ordem das constantes deve corresponder à ordem dos campos na descrição. É possível inicializar apenas uma parte da estrutura, listando valores para os primeiros campos. Nesse caso, os campos restantes serão inicializados com zeros.

Na linguagem MQL5, existem muitas estruturas predefinidas, como MqlDateTime, MqlTradeRequest, MqlTick e outras. Geralmente, utilizá-las não é mais complicado do que o descrito nesta seção. A lista de campos dessas e de muitas outras estruturas, é claro, está detalhada na documentação.

Além disso, o MetaEditor exibe essa lista para qualquer estrutura (e outros tipos complexos) se você criar uma variável do tipo necessário, digitar seu nome e pressionar o ponto "." no teclado.

Lista de campos de uma estrutura

Figura 1. Lista de campos de uma estrutura no editor MetaEditor.

Todos os campos de uma estrutura são acessíveis, por padrão, para todas as funções do programa.


Sobre as estruturas no MQL5 — algumas palavras para quem sabe trabalhar com DLLs externas

Aviso: Esta seção pode ser difícil para iniciantes. Assim, ao ler pela primeira vez, é recomendável pular diretamente para uniões, retornando a esta seção mais tarde.

Por padrão, os dados em estruturas no MQL5 são organizados de forma "compactada", ou seja, diretamente uns após os outros. Portanto, se for necessário que a estrutura ocupe uma quantidade específica de bytes, pode ser necessário adicionar elementos de preenchimento adicionais.

Lembre-se de que, nesse caso, é melhor organizar os dados primeiro pelos de maior tamanho e, em seguida, pelos de menor tamanho. Isso pode evitar muitos problemas. Contudo, nas estruturas do MQL5, existe a possibilidade de "alinhar" os dados usando o operador especial pack:

struct pack(sizeof(long)) MyStruct1
     {
      // structure members will be aligned on an 8-byte boundary
     };

// or

struct MyStruct2 pack(sizeof(long))
     {
      // structure members will be aligned on an 8-byte boundary
     };

Exemplo 5. Alinhamento de uma estrutura.

Nos parênteses de pack, só são permitidos os números 1, 2, 4, 8 ou 16.

O comando especial offsetof permite obter o deslocamento, em bytes, de qualquer campo de uma estrutura em relação ao início. Por exemplo, na estrutura TradeParameters do exemplo 3, para obter o deslocamento do campo stopLoss, pode-se usar o seguinte código:

Print (offsetof(TradeParameters, stopLoss)); // Result: 0

Exemplo 6. Uso do operador offsetof.

As estruturas que NÃO contêm strings, arrays dinâmicos, objetos baseados em classes ou ponteiros são chamadas de simples. Variáveis de estruturas simples, assim como arrays compostos por esses elementos, podem ser passadas livremente para funções importadas de bibliotecas DLL externas.

É permitido copiar estruturas simples umas para as outras utilizando o operador de atribuição, mas apenas nos seguintes casos:

  • as variáveis pertencem ao mesmo tipo;
  • ou os tipos das variáveis estão relacionados por uma linha direta de herança.

    Isso significa que, se temos estruturas como "plantas" e "árvores", qualquer variável baseada em "árvores" pode copiar outra baseada em "plantas" e vice-versa. No entanto, se também tivermos "arbustos", a cópia entre "arbustos" e "árvores" (ou vice-versa) só pode ser feita campo por campo.

Em todos os outros casos, até mesmo estruturas com campos idênticos devem ser copiadas elemento por elemento.

As mesmas regras se aplicam à conversão de tipos: não é possível converter diretamente um "arbusto" em uma "árvore", mesmo que os campos sejam iguais, mas é possível converter "planta" em "arbusto"...

Porém, se for extremamente necessário converter "arbusto" para "árvore", pode-se usar uniões (union). O mais importante é lembrar as limitações das uniões descritas no respectivo capítulo deste artigo. Dito isso, quaisquer campos numéricos serão convertidos sem problemas.

//---
enum ENUM_LEAVES
  {
   rounded,
   oblong,
   pinnate
  };

//---
struct Tree
  {
   int               trunks;
   ENUM_LEAVES       leaves;
  };

//---
struct Bush
  {
   int               trunks;
   ENUM_LEAVES       leaves;
  };

//---
union Plant
  {
   Bush bush;
   Tree tree;
  };

//---
void OnStart()
  {
   Tree tree = {1, rounded};
   Bush bush;
   Plant plant;

// bush = tree; // Error!
// bush = (Bush) tree; // Error!
   plant.tree = tree;
   bush = plant.bush; // No problem...

   Print(EnumToString(bush.leaves));
  }
//+------------------------------------------------------------------+

Exemplo 7. Conversão de estruturas usando uniões (union).

Por ora, vou encerrar aqui. A descrição completa de todas as funcionalidades das estruturas contém muito mais detalhes e nuances do que apresentados neste artigo. Se for necessário comparar estruturas do MQL5 com outros idiomas ou buscar detalhes específicos... Bem, espero que você já saiba onde procurar.

Mas, para iniciantes, acredito que o material apresentado sobre estruturas seja suficiente, por isso passarei ao próximo tópico.


Uniões (union)

Para algumas tarefas, pode ser necessário interpretar os dados em uma única célula de memória como variáveis de tipos diferentes. Essas tarefas são encontradas frequentemente na conversão de tipos de estruturas. Também podem surgir em contextos como criptografia.

A descrição desses dados é quase idêntica à de estruturas simples:

// Creating a type
union AnyNumber {
  long   integerSigned;  // Any valid data types (see further)
  ulong  integerUnsigned;
  double doubleValue;
};

// Using
AnyNumber myVariable;

myVariable.integerSigned = -345;

Print(myVariable.integerUnsigned);
Print(myVariable.doubleValue);

Exemplo 8. Uso de uniões.

Para evitar erros, recomenda-se usar em uniões apenas dados cujo tipo ocupe o mesmo espaço na memória (embora, em algumas conversões, isso não seja necessário ou até prejudicial).

Os membros de uma união (union) NÃO podem ser dos seguintes tipos de dados:

  • arrays dinâmicos,
  • strings,
  • ponteiros para objetos e funções,
  • objetos de classes,
  • objetos de estruturas que possuam construtores ou destrutores,
  • objetos de estruturas que tenham elementos dos itens 1-5.

Fora isso, não há mais restrições.

Mas vou reforçar: se sua estrutura contiver algum campo do tipo string, o compilador apresentará um erro. Tenha isso em mente.


Noções iniciais sobre programação orientada a objetos

Na programação moderna, é muito comum o uso da abordagem orientada a objetos (OO). A essência dessa abordagem é dividir tudo o que acontece no programa em blocos individuais. Cada bloco descreve uma "entidade" específica: arquivo, linha reta, janela, lista de preços...

O objetivo de cada bloco é reunir os dados e as ações necessárias para processá-los em um único lugar. Se essa "reunião" for feita corretamente, ela oferece muitos benefícios:

  • permite reutilizar o código várias vezes;
  • facilita o trabalho do IDE, que consegue rapidamente sugerir os nomes das variáveis e funções relacionadas a objetos específicos;
  • simplifica a busca por erros e reduz as chances de introduzir novos;
  • facilita o trabalho simultâneo de diferentes pessoas (ou equipes) em partes distintas do código;
  • torna mais fácil modificar o código, mesmo após muito tempo;
  • e tudo isso, no final, acelera o desenvolvimento, aumenta a confiabilidade do programa e simplifica a codificação.

E essa organização é bastante natural, pois utiliza princípios que já seguimos na vida cotidiana. Constantemente classificamos objetos: "Isso pertence à classe dos animais, isso às plantas, e aquilo é mobília"... A mobília, por sua vez, pode ser rígida ou estofada... E assim por diante.

Essas classificações utilizam características específicas dos objetos, suas descrições. Por exemplo, as plantas têm caule e raízes, enquanto os animais possuem membros móveis que lhes permitem se locomover. De forma geral, existem alguns atributos característicos para cada classe. No desenvolvimento de software, é a mesma coisa.

Se quisermos criar uma biblioteca para trabalhar com linhas retas, precisa entender o que cada linha pode fazer e o que ela possui para isso. Por exemplo, qualquer linha reta possui um ponto inicial, um ponto final, uma espessura e uma cor.

Esses são seus atributos, ou propriedades, ou campos da classe de linhas retas. Para ações, podem ser usados verbos como "desenhar", "mover", "copiar com um deslocamento específico", "girar em determinado ângulo"...

Se o objeto "linha" pode executar todas essas ações de forma independente, os programadores chamam essas ações de métodos do objeto.

Propriedades e métodos juntos são chamados de membros (elementos) da classe.

Assim, para criar uma linha usando essa abordagem, primeiro é necessário criar a classe (descrição) dessa linha — e de todas as outras no programa —, e depois apenas informar ao compilador: "Estas variáveis são linhas, e esta função irá usá-las. ..".

A classe é um tipo de variável que contém a descrição das propriedades e métodos dos objetos associados a essa classe.

A maneira de descrever uma classe é muito semelhante à de uma estrutura. A principal diferença é que, por padrão, todos os membros de uma classe são acessíveis apenas dentro dela mesma. Já em uma estrutura, todos os seus membros são acessíveis a todas as funções do programa. Abaixo está o esquema geral para criar a classe necessária:

// class (variable type) description
class TestClass { // Create a type

private:          // Describe private variables and functions 
                  //   They will only be accessible to functions within the class 
  
// Description of data (class "properties" or "fields")
  double m_privateField; 

// Description of functions (class "methods")
  bool  Private_Method(){return false;} 

public:           // Description of public variables and functions 
                  //   They will be available to all functions that use objects of this class    

// Description of data (class "properties", "fields", or "members")   
  int m_publicField; 

// Description of functions (class "methods")   
  void Public_Method(void)
    {
     Print("Value of `testElement` is ",  testElement );   
    }
 }; 


Exemplo 9. Descrição da estrutura de uma classe

As palavras-chave public: e private: determinam as áreas de visibilidade dos membros da classe.

Tudo que estiver abaixo de public: será acessível "de fora" da classe, ou seja, por outras funções do programa, mesmo que não pertençam a essa classe.

Tudo que estiver fora dessa seção (e abaixo da palavra private:) será "oculto": apenas as funções do mesmo classe terão acesso a esses elementos.

Uma classe pode conter quantas seções public: e private: forem necessárias.

Apesar disso, recomenda-se usar apenas um bloco para cada área de visibilidade (um private: e um public:), para que todos os dados ou funções com o mesmo nível de acesso fiquem agrupados. Alguns desenvolvedores experientes, porém, preferem criar quatro seções: duas (privada e pública) para funções e duas para variáveis. A escolha é sua.

Vale notar que a palavra private: : pode ser omitida, já que todos os membros de uma classe que não sejam declarados como public: serão privados por padrão (ao contrário das estruturas). Contudo, isso não é recomendável, pois pode dificultar a leitura do código.

É importante lembrar que, no geral, pelo menos uma função da classe descrita deve ser "pública". Caso contrário, a classe será inútil na maioria dos casos. Existem exceções, mas elas são raras. Existem exceções, mas elas são raras.

Uma boa prática em programação orientada a objetos é colocar apenas funções na seção public: (e NÃO variáveis), para proteger os dados. Isso permite que as variáveis da classe sejam alteradas apenas por meio dos métodos dessa classe. Esse método aumenta a confiabilidade do código.

Depois que a classe é descrita, para usá-la no local desejado do programa, basta criar variáveis do tipo correspondente. A criação das variáveis ocorre de maneira convencional. O acesso aos métodos e propriedades de cada variável é feito, geralmente, através do operador de ponto, assim como nas estruturas:
// Description of the variable of the required type
TestClass myTestClassVariable;

// Using the capabilities of this variable
myTestClassVariable.testElement = 5;
myTestClassVariable.PrintTestElement();

Exemplo 10. Uso de uma classe.

Para ilustrar o funcionamento das propriedades públicas e privadas, experimente inserir o código do exemplo 11 dentro da função OnStart do seu script e compilar o arquivo. A compilação provavelmente será bem-sucedida.

Em seguida, descomente a linha "myVariable.a = 5;" e tente compilar o código novamente. Desta vez, ocorrerá um erro de compilação indicando uma tentativa de acessar membros privados da classe. Essa característica do compilador ajuda a evitar erros sutis e difíceis de detectar, comuns em outras abordagens de programação.

class PrivateAndPublic 
  {
private:
    int a;
public:
    int b;
  };

PrivateAndPublic myVariable;

// myVariable.a = 5; // Compiler error! 
myVariable.b = 10;   // Success

Exemplo 11. Uso de propriedades públicas e privadas de uma classe.

Se todas as classes precisassem ser escritas manualmente, essa abordagem não se destacaria das demais e dificilmente teria um propósito especial.

No entanto, felizmente, muitos classes padrão já estão disponíveis no diretório MQL5\Include. Além disso, há uma vasta coleção de bibliotecas úteis na base de códigos (Codebase). Em muitos casos, basta incluir o arquivo apropriado (como descrito abaixo) para aproveitar os trabalhos de outros programadores talentosos. Isso facilita bastante a vida dos programadores.

Livros inteiros são escritos sobre POO, e ela certamente merece artigos dedicados. No entanto, o objetivo deste artigo é apenas introduzir o iniciante ao uso de tipos de dados complexos em programas que ele encontrará. Agora que você já sabe como criar uma classe básica e como utilizar classes feitas por terceiros, vou avançar para o próximo tópico.


Tipo de dado funcional (operador typedef)

Aviso: Este tópico pode ser complicado para iniciantes. Se estiver lendo o artigo pela primeira vez, pode pular esta parte sem prejuízo para o restante do aprendizado.

A compreensão deste material não é essencial para assimilar o restante do conteúdo — e talvez nem seja essencial para sua jornada na programação. A maioria das tarefas pode ser resolvida de várias formas, sem a necessidade de tipos funcionais.

Ainda assim, a possibilidade de associar funções a variáveis (e, em alguns casos, utilizá-la como argumento de outra função) pode ser útil. Por isso, acredito que vale a pena conhecer esse recurso, ao menos para entender códigos escritos por outros

Por vezes, é útil criar variáveis de tipo funcional, por exemplo, para passá-las como argumentos para outras funções.

Imagine, por exemplo, uma situação de negociação em que as ordens de compra e venda são muito semelhantes, diferenciando-se apenas por um parâmetro. No entanto, o preço de compra é sempre Ask, enquanto as vendas ocorrem no preço Bid.

Muitos programadores costumam escrever funções específicas para comprar e vender (Buy e Sell), levando em conta todos os detalhes de uma ordem. Depois, criam uma função chamada Trade, que combina essas duas operações e pode ser usada tanto para negociar "para cima" quanto "para baixo". Isso é conveniente porque Trade decide automaticamente se deve chamar Buy ou Sell, com base no movimento calculado do preço, permitindo ao programador concentrar-se em outros aspectos.

Há inúmeros cenários em que queremos dizer: "Automatize isso com inteligência!" — deixando a função decidir qual das opções disponíveis deve ser utilizada em determinada situação. Por exemplo: No cálculo de um take profit, adicionar ou subtrair pontos do preço? No cálculo de um stop loss, qual lógica usar? Na definição de ordens baseadas em extremos, buscar máximos ou mínimos? E por aí fora.

Essas e outras situações podem se beneficiar do uso do recurso que será descrito a seguir.

Como sempre, o primeiro passo é descrever o tipo da variável necessária. Neste caso, o tipo é definido conforme o seguinte modelo:

typedef function_result_type (*Function_type_name)(input_parameter1_type,input_parameter1_type ...); 

Exemplo 12. Modelo para descrição de tipo funcional.

Aqui:

  • function_result_type — é o tipo do valor retornado (qualquer tipo permitido, como int, double ou qualquer outro);
  • Function_type_name — é o nome do tipo que será usado ao criar variáveis;
  • input_parameter1_type — é o tipo do primeiro parâmetro. O restante da lista de parâmetros segue as regras normais para listas de funções.

Observe o asterisco (*) antes do nome do tipo. Ele é essencial, e sem ele nada funcionará.

O asterisco indica que a variável desse tipo não armazenará um resultado ou valor numérico, mas sim a própria função, com todas as suas capacidades. Isso significa que essa variável combinará as funcionalidades comuns às variáveis e às funções.

Essa construção, que utiliza o próprio objeto (como uma função ou um objeto de classe) em vez de uma cópia de seus dados ou o resultado de sua execução, é chamada de ponteiro.

Falaremos mais sobre ponteiros em artigos futuros. Por enquanto, vejamos um exemplo prático do uso do operador typedef.

Suponha que temos as funções Diff e Add, que queremos associar a alguma variável. Ambas as funções retornam valores inteiros e aceitam dois parâmetros inteiros. A implementação delas é simples:

//---
int Add (int a,int b)
  {
    return (a+b);
  }

//---
int Diff (int a,int b)
  {
    return (a-b);
  }

Exemplo 13. Funções de soma e diferença para verificar o tipo funcional.

Agora, definimos o tipo TFunc para variáveis que podem armazenar qualquer uma dessas funções:
typedef int (*TFunc) (int,  int);

Exemplo 14. Descrição do tipo para variáveis que podem armazenar as funções Add e Diff.


E, em seguida, verificamos como essa descrição funciona na prática:

void OnStart()
  {
    TFunc operate;       //As usual, we declare a variable of the described type
 
    operate = Add;       // Write a value to a variable (in this case, assign a function)
    Print(operate(3, 5)); // Use the variable as a normal function
                         // Function output: 8

    operate=Diff;
    Print(operate(3, 5)); // Function output: -2
  }

Exemplo 15. Uso de uma variável de tipo funcional.

Por fim, vale mencionar que o operador typedef funciona somente com funções criadas manualmente.

Não é possível usá-lo diretamente com funções padrão como MathMin ou similares, mas é possível criar uma "camada intermediária" para elas. Por exemplo:

//---
double MyMin(double a, double b){
   return (MathMin(a,b));
}

//---
double MyMax(double a, double b){
   return (MathMax(a,b));
}

//---
typedef double (*TCompare) (double,  double);

//---
void OnStart()
  {
    TCompare extremumOfTwo;

    compare= MyMin;
    Print(extremumOfTwo(5, 7));// 5

    compare= MyMax;
    Print(extremumOfTwo(5, 7));// 7
  }

Exemplo 16. Uso de "camadas intermediárias" para trabalhar com funções padrão.


Inclusão de arquivos externos (diretiva de pré-processador #include)

Qualquer programa pode ser dividido em módulos.

Se você trabalha com projetos grandes, essa divisão é essencial. A modularidade de um programa resolve várias questões ao mesmo tempo.

  • Facilidade de leitura: permite navegar mais facilmente pelo código.
  • Trabalho em equipe: em projetos colaborativos, diferentes pessoas podem desenvolver módulos distintos, acelerando o desenvolvimento.
  • Reutilização: módulos já criados podem ser reaproveitados em outros projetos.

O exemplo mais óbvio de "módulo" são as funções. Porém, também é possível modularizar todas as constantes, a definição de algum tipo de dado complexo, ou agrupar várias funções relacionadas a uma mesma área de atuação (como funções para alterar a aparência de objetos ou funções matemáticas)...

Em projetos maiores, é extremamente útil organizar esses blocos de código em arquivos separados e depois incluí-los no programa atual.

Para incluir arquivos de texto adicionais no programa, usa-se a diretiva de pré-processador #include:

#include <SomeFile.mqh>     // Angle brackets specify search relative to MQL5\Include directory 
#include "AnyOtherPath.mqh" // Quotes specify search relative to current file

Exemplo 17. Duas formas da diretiva #include.

Quando o compilador encontra a instrução #include em qualquer parte do código, ele tenta substituir essa instrução pelo conteúdo do arquivo especificado, mas apenas uma vez por programa. Se o arquivo já foi utilizado, ele não será incluído novamente.

É possível testar esse comportamento usando um script descrito na próxima seção.

Normalmente, os arquivos inclusos têm a extensão *.mqh por conveniência, mas, em geral, a extensão pode ser qualquer uma.


Script para verificar o funcionamento da diretiva #include

Para testar como o compilador reage ao encontrar essa diretiva de pré-processador, criaremos dois arquivos.

Primeiro, crie um arquivo chamado "1.mqh" no diretório de scripts (MQL5\Scripts). O conteúdo deste arquivo será muito simples:

Print("This is include with number "+i);

Exemplo 18. Um arquivo incluso básico pode conter apenas um comando.

O que esse código faz deve ser autoexplicativo. Supondo que a variável i tenha sido definida em algum lugar, este código cria uma mensagem para o usuário anexando o valor da variável à mensagem e, em seguida, exibe essa mensagem no log.

A variável i será um marcador, indicando em qual parte do script a inclusão do arquivo foi processada (ou não). Mais uma vez, ressalto que neste arquivo não é necessário escrever mais nada. Agora, no mesmo diretório (onde está o arquivo "1.mqh"), crie um script com o seguinte código:

//+------------------------------------------------------------------+ 
//| Script program start function                                    | 
//+------------------------------------------------------------------+ 
void OnStart() 
  { 
    //---   
    int i=1; 
#include "1.mqh"   
    i=2; 
#include "1.mqh" 
  } 
//+------------------------------------------------------------------+

// Script output:
// 
//   This is include with number 1
//
// The second attempt to use the same file will be ignored

//+------------------------------------------------------------------+ 

Exemplo 19. Estudo do comportamento ao incluir arquivos repetidamente.

Neste código, tentamos usar o arquivo "1.mqh" duas vezes, para gerar duas mensagens de saída.

Ao executar este script no terminal, veremos que a primeira mensagem será exibida conforme esperado, mostrando o valor 1 na mensagem, mas a segunda mensagem não aparecerá.

Por que essa regra? Por que não inserir o conteúdo toda vez?

Esse é um princípio importante, pois arquivos inclusos frequentemente contêm definições de variáveis e funções. Você já sabe que, em um programa, no nível global (fora de todas as funções), só pode haver uma variável com um determinado nome.

Por exemplo, se uma variável int a; foi definida, não é permitido declarar outra variável igual no mesmo nível global; somente a existente poderá ser reutilizada. Com funções, é um pouco mais complicado, mas o conceito é o mesmo: cada função deve ser única no escopo do programa. Agora, imagine que sua aplicação usa dois módulos independentes, mas cada um inclui a mesma classe padrão de um arquivo, como <Arrays\List.mqh> (veja a Figura 2).

Uso de uma classe por dois módulos

Figura 2. Uso de uma classe por dois módulos.

Se não existisse a "regra de inclusão única", o compilador geraria um erro, já que descrever a mesma classe duas vezes é proibido. No entanto, nesse caso, essa construção funciona perfeitamente, pois, após a descrição do campo FieldOf_Module1, a definição da classe CList já foi incluída nas listas do compilador. Assim, ele simplesmente reutiliza essa definição para o módulo 2.

Entendendo este princípio, você pode criar até mesmo inclusões "multicamadas", como em casos em que elementos de classes dependem uns dos outros de forma cíclica, conforme mostrado na Figura 3.

É possível, inclusive, declarar dentro de uma classe uma variável do próprio tipo dessa classe.

Tudo isso são construções válidas, e isso é possível, exatamente porque #include é processado uma única vez para cada arquivo.

Dependência Cíclica: cada classe contém elementos que dependem de outra

Figura 3. Dependência Cíclica: cada classe contém elementos que dependem de outra.

Para concluir esta seção, gostaria de lembrar mais uma vez que os arquivos das bibliotecas padrão do MetaTrader, que você pode incluir no seu código, estão localizados no diretório MQL5\Include. Para abrir facilmente esse diretório no explorador de arquivos, você pode acessar o menu "Arquivo" -> "Abrir pasta de dados" no terminal MetaTrader (veja a Figura 4).

Acessando o Diretório de Dados

Figura 4. Acesso ao diretório de dados.

Caso você deseje visualizar os arquivos desse diretório no MetaEditor, basta localizar a pasta Include no painel do navegador,  você pode criá-los seus próprios arquivos inclusos nesse mesmo diretório (preferencialmente organizados em pastas separadas) ou no diretório do seu projeto, incluindo seus subdiretórios (veja os comentários no Exemplo 17). Normalmente, as diretivas #include são colocadas no início do arquivo, antes de qualquer outra instrução. Contudo, essa não é uma regra rígida; a posição da diretiva dependerá das necessidades específicas do programa.


Considerações finais

Recapitulando brevemente os tópicos abordados neste artigo:

  1. Foi descrita a diretiva de pré-processador #include. Ela permite incluir arquivos de texto adicionais no programa, geralmente bibliotecas.
  2. Foram apresentados tipos de dados complexos: estruturas, uniões e objetos (variáveis baseadas em classes), além de tipos de dados funcionais.

Espero que agora os tipos de dados descritos neste artigo sejam "complexos" apenas em termos de construção, mas não em sua aplicação.

Diferentemente dos tipos simples, que já são embutidos na linguagem, os tipos "complexos" precisam ser definidos antes de serem utilizados para criar variáveis. No entanto, uma vez que o tipo esteja definido, trabalhar com ele não difere de trabalhar com tipos "simples". Isso se resume a criar variáveis e utilizar seus componentes (membros), caso existam, ou usar o nome da variável como uma função, no caso de tipos funcionais.

A inicialização de variáveis criadas com estruturas pode ser feita usando chaves {}.

Por fim, espero que agora você entenda como a capacidade de criar seus próprios tipos complexos e dividir O programa em módulos Armazenados em arquivos externos torna o desenvolvimento mais flexível e prático.


Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/14354

Últimos Comentários | Ir para discussão (5)
MrBrooklin
MrBrooklin | 19 jul. 2024 em 05:52

Sou um iniciante que aprendeu os conceitos básicos de programação. Li seu próximo artigo e cheguei a uma conclusão: um iniciante com total falta de conhecimento não entenderá nada desse artigo. Essa é minha opinião pessoal, sem pretensão de ser a verdade em última instância.

Vamos tomar a seção Estruturas do artigo como exemplo. O início é bom e claro o suficiente. Você disse para que serve a estrutura e mostrou como criá-la. E então, bang, e um novo código!

IntradayTime dealEnterTime;

dealEnterTime.hours = 8;
dealEnterTime.minutes = 15;
dealEnterTime.timeCodeString = "GMT+2";

Eu destaquei essa parte do código propositalmente. O que um iniciante com conhecimento zero deve entender dessa linha? O que isso significa para ele? Eu já a entendo, mas para um iniciante sem conhecimento é outro fragmento de código incompreensível. É por isso que é desejável descrever e explicar completamente cada linha. Caso contrário, este artigo não é para iniciantes, mas para programadores avançados.

Saudações, Vladimir.

Oleh Fedorov
Oleh Fedorov | 19 jul. 2024 em 08:32
MrBrooklin #:

Sou um iniciante que aprendeu os conceitos básicos de programação. Li seu próximo artigo e cheguei a uma conclusão: um iniciante com total falta de conhecimento não entenderá nada desse artigo. Essa é minha opinião pessoal, sem pretensão de ser a verdade em última instância.

Vamos tomar a seção Estruturas do artigo como exemplo. O início é bom e claro o suficiente. Você disse para que serve a estrutura e mostrou como criá-la. E então, bang, e um novo código!

Eu destaquei essa parte do código propositalmente. O que um iniciante com conhecimento zero deve entender dessa linha? O que isso significa para ele? Eu já a entendo, mas para um iniciante com total falta de conhecimento, é outro fragmento de código incompreensível. É por isso que é desejável descrever e explicar completamente cada linha. Caso contrário, este artigo não é para iniciantes, mas para programadores avançados.

Saudações, Vladimir.

É impressão minha ou foi essa mesma estrutura que eu criei três linhas antes? E duas linhas atrás eu expliquei que ela é um tipo de dados? E isso deve significar que esse tipo deve ser usado da mesma forma que todos os outros? (Realmente, a lógica deve estar envolvida aqui, sim ;-))

Embora você provavelmente esteja certo, pelo menos um comentário sobre o tipo não faria mal.... Obrigado.

AKHMED Asmalov
AKHMED Asmalov | 11 fev. 2025 em 04:25
void OnStart()
classe PrivateAndPudlic
}
private:
int a;
public:
int b;
};
PrivateAndPudlic myVariable;
//myVariable.a = 5; //Erro do compilador!

myVariable.b = 10; //Não há problema, você pode fazer isso dessa forma

Ele dá um erro durante a compilação. Você pode me dizer o que está errado, onde está o erro?

Oleh Fedorov
Oleh Fedorov | 19 mar. 2025 em 10:28
AKHMED Asmalov #:
void OnStart()
classe PrivateAndPudlic
}
private:
int a;
público:
int b;
};
PrivateAndPudlic myVariable;
//myVariable.a = 5; //Erro do compilador!

myVariable.b = 10; //Não há problema, você pode fazer dessa forma.

Recebo um erro ao compilar. Você pode me dizer o que está errado, onde está o erro?

Desculpe-me pela demora na resposta.

O código deste exemplo não está totalmente completo. Para que ele funcione, você precisa usar a variável myVariable em algum lugar dentro da função. Por exemplo:

  class PrivateAndPudlic
   {
  private:
     int a;
  public:
     int b;
   }; 

 PrivateAndPudlic myVariable; // Variável global

void OnStart(){ // Todas as chamadas para ações (nesse caso, atribuição) devem ocorrer somente dentro de funções
  //myVariable.a = 5; //Erro do compilador!

   myVariable.b = 10; //Está tudo bem, está tudo bem
}

Bem, você inverteu os parênteses ao reimprimi-lo (colocou o fechamento "}" em vez da abertura "{") ;-)

Oleh Fedorov
Oleh Fedorov | 19 mar. 2025 em 11:34
Oleh Fedorov #:

Desculpe-me pela demora em responder.

O código deste exemplo não está totalmente completo. Para que ele funcione, você precisa usar a variável myVariable em algum lugar dentro da função. Por exemplo:

Bem, você inverteu os parênteses ao reimprimi-lo (em vez da abertura "{", você colocou o fechamento "}") ;-)

Bem, ou conforme descrito no artigo:

void OnStart(){

class PrivateAndPudlic
 {
  private:
     int a;
  public:
     int b;
   }; 
 PrivateAndPudlic myVariable;
 
//myVariable.a = 5; //Erro do compilador!

 myVariable.b = 10; //Está tudo bem, está tudo bem

}
Desenvolvendo um EA multimoeda (Parte 15): Preparando o EA para o trading real Desenvolvendo um EA multimoeda (Parte 15): Preparando o EA para o trading real
À medida que nos aproximamos de um EA pronto, é necessário prestar atenção em questões secundárias na etapa de teste da estratégia de trading, mas que se tornam importantes ao migrar para o trading real.
Desenvolvendo um EA multimoeda (Parte 14): Alteração adaptativa dos volumes no gerenciador de risco Desenvolvendo um EA multimoeda (Parte 14): Alteração adaptativa dos volumes no gerenciador de risco
O gerenciador de risco anteriormente desenvolvido continha apenas funcionalidades básicas. Vamos explorar caminhos para aprimorá-lo, buscando melhorar os resultados de negociação sem alterar a lógica das estratégias de trading.
Teoria do caos no trading (Parte 1): Introdução, aplicação nos mercados financeiros e o indicador de Lyapunov Teoria do caos no trading (Parte 1): Introdução, aplicação nos mercados financeiros e o indicador de Lyapunov
É possível aplicar a teoria do caos nos mercados financeiros? Vamos explorar nesta matéria como a teoria clássica do caos e os sistemas caóticos diferem do conceito proposto por Bill Williams.
Desenvolvendo um sistema de Replay (Parte 75): Um novo Chart Trade (II) Desenvolvendo um sistema de Replay (Parte 75): Um novo Chart Trade (II)
Neste artigo explicarei grande parte da classe C_ChartFloatingRAD. Esta é responsável por fazer com que o Chart Trade funcione. Porém aqui não irei de fato terminar a explicação. A mesma será finalizada no próximo artigo. Já que o conteúdo neste artigo é bastante denso e precisa ser compreendido a fundo. O conteúdo exposto aqui, visa e tem como objetivo, pura e simplesmente a didática. De modo algum deve ser encarado como sendo, uma aplicação cuja finalidade não venha a ser o aprendizado e estudo dos conceitos mostrados.