MQL4: Массивы и циклы

Здравствуйте, уважаемые коллеги форекс-программисты!

В сегодняшнем уроке, по вашим просьбам, мы подробно разберём работу с массивами и циклами посредством языка MQL4.

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

Начнём с циклов. Поскольку обращение к элементам массива, как правило, осуществляется с использованием циклов, то научимся сначала работать с ними, а потом перейдем к массивам.

MQL4 программирование - Массивы и циклы

Циклы

Цикл while

Синтаксис цикла while в MQL4 имеет следующий вид:

while (<условие>)
{
 <тело цикла>;
}

Тело цикла в данном случае будет выполняться, пока истинно условие в круглых скобках. Если это условие на момент выполнения цикла окажется ложным, то тело цикла не выполнится ни одного раза. В качестве примера выведем в журнал терминала целые числа от 1 до 5, каждое с новой строки. Код будет выглядеть так:

int i = 1;
while (i <= 5)
   {
    Print( i );
    i++;
   }

Исходя из условий задачи, мы сначала определили целочисленную переменную i и присвоили ей значение 1. В операторе цикла while проверяем, меньше или равно 5 значение переменной i. Если да, то выполняется тело цикла: значение печатается в журнал и увеличивается на единицу. После первой итерации в журнал выведено число «1», а значение переменной i равно двум. Спустя несколько итераций переменная i стала равна шести, условие i<=5 ложно. Следовательно, тело цикла выполняться больше не будет.

Цикл for

Этот вид цикла работает аналогично уже рассмотренному while. Здесь тело цикла выполняется, пока некое условие истинно. Однако в самом операторе можно задать действия, которые совершаются до начала цикла (например, инициализировать переменную-счётчик), а также после выполнения каждой итерации (увеличение или уменьшение счётчика). Такой вид цикла предназначен прежде всего для ситуаций, когда нам заранее известно число итераций. Синтаксис цикла for в языке MQL4 таков:

for(выражение1; выражение2; выражение3)
{
 <тело цикла>;
}
  • выражение1 – инициализация цикла, как правило, это объявление и инициализация счётчика итераций;
  • выражение2 – условие продолжения цикла, пока оно истинно, тело цикла выполняется;
  • выражение3 – выражение, вычисляемое после каждой итерации, обычно здесь происходит изменение счётчика итераций.

Рассмотрим цикл for на следующем примере: найдём сумму целых чисел от 1 до 1000.

int sum=0; // переменная для суммы чисел
for(int i=1; i<=1000; i++) 
   sum+=i; // прибавляем к текущему значению суммы очередное число 
Print("Сумма чисел от 1 до 100: ",i);

Мы объявили переменную sum для записи в неё текущей суммы чисел и проинициализировали её нулевым значением.

В операторе for переменная i выступает в качестве счётчика итераций. Мы объявили и проинициализировали её начальным значением «1». Нам требуется, чтобы цикл выполнялся 1000 раз, то есть пока переменная i меньше или равна 1000. Условие продолжения цикла примет вид i<=1000. После каждой итерации нам надо увеличить счётчик на единицу. Запишем через точку с запятой i++.

Тело цикла представляет всего один оператор sum+=i, что эквивалентно записи sum=sum+i. Поэтому операторные скобки «{ }» здесь не нужны. В теле цикла значение переменной sum увеличивается на значение переменной i, в которой на текущей итерации содержится очередное число от 1 до 1000. В итоге после тысячи выполнений тела цикла переменная sum будет содержать сумму всех целых чисел от 1 до 1000.

Любое из трех или все выражения в операторе for(выражение1; выражение2; выражение3) могут отсутствовать. Нельзя опускать только разделяющие выражения точки с запятыми, то есть в записи всегда должны быть оба символа «;». Например, запись for(;;) представляет собой бесконечный цикл. Выражение1 и выражение3 могут состоять из нескольких выражений, объединенных оператором запятая «,».

Решим такую задачу. Напечатаем в журнал числа от 1 до 5 и от 5 до 1 одновременно в одном цикле. При этом тело цикла должно состоять только из одного вызова функции Print().

   for(int i=1, j=5; i<=5; i++, j--)
    Print(i," ",j);

Как видите, тело цикла выполняется 5 раз, при этом переменная i изменяется от 1 до 5, а переменная j от 5 до 1. Результат выполнения выглядит следующим образом:

5 1
4 2
3 3
2 4
1 5

Заметьте, что наряду с инкрементом, то есть увеличением переменной i на единицу, здесь мы использовали и декремент, то есть уменьшали на единицу переменную j.

Запишем вывод в цикле пяти целых чисел от 1 до 5 в журнал следующим образом:

int n=1;  
for(;n<=5;)
 {
  Print(n);
  n++;
 }

Здесь мы объявили переменную-счётчик до оператора цикла, а её инкрементацию поместили в тело цикла.

Счётчик в цикле for может увеличиваться или уменьшаться на любое значение, а не только на единицу. Тем самым можно реализовать цикл с шагом. Выведем, например, в журнал все нечётные числа от 1 до 10. Для этого будем увеличивать счётчик после каждой итерации на 2, начиная с единицы:

for(int i=1; i<=10; i+=2)
 Print(i);

В данном случае переменная-счётчик последовательно примет значения 1, 3, 5, 7, 9, 11, а в журнал выведет 1, 3, 5, 7, 9.

Цикл do while

Как отмечалось ранее, если условие продолжения цикла c предусловием (while) изначально ложно, то тело цикла не выполнится ни разу. Однако есть задачи, которые требуют выполнения цикла по крайней мере однократно. Для этих целей существует цикл, тело которого выполнится хотя бы один раз независимо от условия. Синтаксис цикла do while или, как его еще называют, цикл с постусловием в MQL4 имеет следующий вид:

do
{
 <тело цикла>;
}
while (<условие>);

Выполним ту же задачу с выводом целых чисел от 1 до 5 с помощью цикла do while:

int i=1;
do
 {
  Print(i);
  i++;
 }
 while(i<=5);

Сначала мы объявили переменную-счётчик i и инициализировали её значением 1 согласно условию задачи. Затем сразу выполнилось тело цикла. В журнал было выведено значение переменной i, которое на тот момент было равно единице, следом значение переменной i увеличилось на единицу, а уже потом состоялась проверка условия i<=5. Так как 2 меньше 5, то цикл продолжился, пока значение переменной i не стало равняться шести. Обратите внимание, что инкремент счётчика в теле цикла мы производим после вывода его значения в журнал. Если мы будем делать это до вывода в журнал, то нам необходимо уменьшить начальное значение счётчика и изменить условие. В этом случае цикл будет выглядеть так:

int i=0;
do
 {
  i++;
  Print(i);
}
while(i<5);

Любопытно, что в языке MQL4 операцию инкремента можно выполнять прямо в вызове функции, при этом надо помнить, что постфиксный инкремент (i++) увеличивает значение переменной на единицу сразу после использования этой переменной в выражении, а префиксный инкремент (++i) увеличивает значение переменной на единицу непосредственно перед использованием этой переменной в выражении. Теперь наши примеры можно записать компактнее:

int i=1;
do
 Print(i++);
while(i<=5);

Здесь функция Print() выводит в журнал значение переменной i, а следом происходит увеличение i на единицу (инкремент):

int i=0;
do
  Print(++i);
while(i<5);

При вызове функции Print() сначала происходит увеличение переменной i на единицу, а потом значение i выводится в журнал.

Разберём пример посложнее. Напишем советник, который после прикрепления его к графику начинает обрабатывать поступающие тики не сразу, а спустя какое-то время, заданное нами в секундах. При этом советник должен сообщать с помощью комментария на графике, сколько секунд осталось до его запуска. Очевидно, что такую задержку надо поместить внутрь обработчика OnInit(), так как эта функция выполняется при инициализации советника.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
 uint begin=GetTickCount(),// запоминаем значение функции GetTickCount()
      passed_seconds,      // прошло секунд
      pause=10;            // пауза в секундах
 Comment("");              // очищаем комментарий на графике, выводя пустую строку
 
 do
 {
  passed_seconds=(GetTickCount()-begin)/1000;// прошло секунд со старта
  Comment("Советник начнёт работу через:",pause-passed_seconds);
 }
 while(passed_seconds<pause);// тело цикла выполняется пока количество
// прошедших секунд меньше паузы в секундах
 Comment("");              // очищаем комментарий на графике, выводя пустую строку
   
   return(INIT_SUCCEEDED);
  }

В данном коде мы реализовали задержку с выводом оставшегося до запуска времени в секундах при помощи цикла do while. Функция GetTickCount() возвращает количество миллисекунд, прошедших с момента старта системы. Мы использовали её для вычисления количества прошедших и оставшихся секунд.

Операторы break и continue

Операторы break и continue могут использоваться в теле цикла любого типа.

С помощью оператора break можно немедленно прервать выполнение тела цикла без каких-либо дополнительных условий. Управление при этом передаётся первому следующему за циклом оператору. Рассмотрим такую реализацию знакомой нам задачи вывода целых чисел от 1 до 5 в журнал терминала:

 int i=1;
 while(true)
   {
    Print(i);
    i++;
    if(i>5) break;
   }

Здесь мы имеем бесконечный цикл while, так как условие в круглых скобках всегда истинно. В теле цикла осуществляется вывод в журнал текущего значения переменной i и её инкрементация. Как только значение i станет равным шести, оператор break прервёт выполнение цикла. Тут стоит сделать одно важное замечание. Если цикл предполагает большое число итераций или была допущена ошибка, из-за которой цикл стал бесконечным, программа может не отвечать, «зависнуть». Чтобы избежать такой ситуации, в условие продолжения цикла добавляют вызов функции IsStopped(), которая возвращает истину (true), если от терминала поступила команда завершить выполнение mql4-программы (например, была нажата кнопка «Стоп» при работе в тестере). Учитывая это, наш пример будет правильнее записать так:

 while(!IsStopped)
   {
    Print(i);
    i++;
    if(i>5) break;
   }

Это всё тот же бесконечный цикл, но выполняется он, пока не будет прерван оператором break или пока не поступит команда от терминала на завершение программы.

Надо заметить, что язык MQL4 позволяет прерывать цикл внутри функции еще и оператором return. Например:

void PrintNumber()
{
int i=1;
 while(!IsStopped)
   {
    Print(i);
    i++;
    if(i>5) return;
   }
}

В данном случае оператор return прервёт цикл и передаст управление в то место программы, откуда была вызвана функция PrintNumber().

Оператор continue прерывает текущую итерацию цикла и начинает новую. Все операторы в теле цикла, идущие за оператором continue, не будут выполнены. Для иллюстрации работы оператора немного модифицируем нашу задачу с выводом в журнал целых чисел от 1 до 5. Выведем все числа, кроме 2.

for(int i=1; i<=5; i++)
{
 if(i==2) continue;
 Print(i);
}
1
2
3
4
5

Вывод числа «2» будет пропущен.

Оператор continue удобно применять, если тело цикла довольно большое. Можно не усложнять конструкции ветвления внутри цикла, а сразу пропустить все последующие операторы и перейти к новой итерации.

Массивы

Массивы присутствуют практически во всех языках программирования, и MQL4 не стал исключением. По сути, массив – это совокупность переменных одного типа с одним именем, но при этом каждая переменная имеет свой уникальный номер. Например, нам нужны три целочисленные переменные. Мы можем объявить их следующим образом:

int Var1, Var2, Var3;

В таком случае к каждой переменной можно обращаться по её уникальному имени. Например, присвоим переменным какие-то значения:

Var1=35;
Var2=8;
Var3=21;

С другой стороны, переменные можно задать как массив из трёх элементов:

int Var[3];

Обращаться же к этим переменным следует по их общему имени и номеру в массиве:

Var[0]=35;
Var[1]=8;
Var[2]=21;

Номер переменной в массиве называют индексом (от латинского index – указательный палец). При объявлении массива в квадратных скобках указывается его размер (количество элементов), а при обращении к переменной – индекс элемента.

Обратите внимание, что нумерация элементов в массиве начинается с нуля, а не с единицы. Это обстоятельство часто служит причиной ошибок и вызывает вопросы, особенно у начинающих программистов. Массив, как и обычная переменная, – это область памяти, имеющая своё начало – адрес. В случае с массивами доступ к каждому элементу по индексу задаётся как смещение от начала. Тогда адрес первого элемента совпадает с адресом всего массива, то есть смещение равно нулю. Адрес второго элемента – это адрес первого плюс одно смещение на размер первого элемента. Адрес второго – это два смещения и т. д.

Статические и динамические массивы

Размер статических массивов устанавливается на этапе компиляции и больше в ходе выполнения программы не меняется. Пример объявления статического массива:

int Array[10];

Здесь мы объявили массив из десяти целочисленных элементов, память под которые выделит компилятор.

Массивы, размер которых можно изменять в процессе выполнения программы, называются динамическими. Вместе с изменением размера массива память либо выделяется, либо освобождается динамически. Пример объявления динамического массива:

int Array[];

Получается, если в квадратных скобках не указан размер массива, то он будет объявлен как динамический. В данном случае мы объявили динамический массив целочисленных элементов. Чтобы использовать массив, надо задать его размер с помощью функции ArrayResize():

ArrayResize(Array,10);

Теперь массив Array состоит из 10 элементов с индексами от 0 до 9.

У данной функции есть третий необязательный параметр reserve_size. С помощью него можно задать размер массива «с запасом». Например, первый вызов функции в виде ArrayResize(Array, 10, 1000) увеличит размер массива до 10 элементов, но физически память под массив будет выделена, как если бы он содержал 1010 элементов. То есть память под 1000 элементов оказывается в резерве. Теперь при увеличении или уменьшении размера массива в пределах 1010 элементов физического распределения памяти не будет. Если же размер увеличить, скажем, до 1011, то выделится еще 1000 резервных элементов, и так далее. Поскольку физическое распределение памяти – процесс достаточно медленный, то в случае, если предполагается частое изменение размера массива в программе, третий параметр функции ArrayResize() будет весьма полезен.

В языке MQL4 все элементы массива инициализируются при его объявлении нулевым значением: для числовых массивов – это ноль, для строковых – пустая строка, для массива логических значений – это false. Однако есть ситуации, когда массив надо проинициализировать определёнными значениями. Для статических массивов это можно сделать в момент объявления массива следующим образом:

int Numbers[5] = {3, 21, 72, 1, 8};

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

int Numbers[5] = {82};

Все пять элементов массива примут значение 82.

Динамические массивы инициализировать таким образом нельзя. Даже если вы объявите массив:

int Numbers[] = {3, 21, 72, 1, 8};

то компилятор определит его как статический и автоматически установит размер по количеству значений в фигурных скобках.

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

Использование циклов при работе с массивами

Поскольку элементы массива последовательно пронумерованы, то обработку массивов удобно вести с использованием циклов: производить действия над всеми элементами массива или его частями, искать элемент с нужными характеристиками, упорядочивать элементы внутри массива по какому-то критерию. Наиболее часто встречающаяся операция, применяемая к массиву – это перебор в цикле его значений, которую называют итерацией по массиву.

Решим задачу.

Имеется список слов: память, машина, пицца, вода, ножницы, кот, батарейка, удочка, гром, стол, флаг, телефон, зонт, документ. Необходимо найти все слова, длина которых больше пяти символов, и вывести в журнал все эти слова, а также общее количество таких слов.

Для обработки слов создадим массив строк и запишем слова в него:

string Words[]={"машина", "пицца", "вода", "ножницы", "кот", "батарейка", "удочка", "гром", "стол", "флаг", "телефон", "зонт", "документ"};
int size=ArraySize(Words);
int n=0;
  
   for(int i=0; i<size; i++)
   {
    if(StringLen(Words[i])>5)
    {
     Print(Words[i]);
     n++;
    }
   }
if(n>0) Print("Количество слов, длина которых больше пяти: ",n);

В этом примере нам понадобилась дополнительная переменная size, в которую мы поместили найденный с помощью функции ArraySize() размер массива. Было бы нерационально на каждой итерации вызывать функцию, чтобы сравнить полученное значение со счётчиком. Хотя в данном случае неважно, в каком направлении обходить элементы массива. Поэтому можно обойтись и без дополнительной переменной, обходя массив в обратном порядке:

int n=0;
for(int i=ArraySize(Words)-1; i>=0; i--)
  if(StringLen(Words[i])>5)
    {
     Print(Words[i]);
     n++;
    }
if(n>0) Print("Количество слов, длина которых больше пяти: ",n);

Следующая задача: удалить элемент с указанным индексом из динамического массива целых чисел.

double Array[]; // подготовим массив для демонстрации работы алгоритма

   int size=21; // зададим ему размер в 21 элемент
   int removed_element=17; // индекс удаляемого элемента

   Print("Новый Размер:", ArrayResize(Array,size));

   for(int i=0; i<size; i++) // заполним массив числами от 100, до 120
      Array[i]=i+100.0;


   if(removed_element<size) // индекс удаляемого элемента не выходит за пределы массива
     {

      Print("Будет удалён элемент с индексом ", removed_element,", имеющий значение ", Array[removed_element]);

      for(int i=removed_element+1; i<size; i++) // сдвинем все элементы за удаляемым влево на один
         Array[i-1]=Array[i];

      size=ArrayResize(Array,size-1); // уменьшим размер массива на единицу, тем самым отсечём ненужный последний элемент

      Print("Новый размер массива: ", size);

      for(int i=size-1; i>=0; i--) // выведем в журнал все элементы массива
        {
         Print("Array[",i,"]=",Array[i]);
        }

     }
   else
      Print("Индекс удаляемого элемента находится за пределами массива");

Передача массива в функцию

Массивы можно передавать в функции только по ссылке, то есть в функцию передаётся только адрес существующего массива, а не его копия. Функция не может вернуть и сам массив. Она только производит действия над массивом в целом или над его отдельными элементами. Таким образом, если аргумент функции представляет собой массив, то перед его именем необходимо писать знак «&» (амперсанд), как знак того, что аргумент передаётся по ссылке, а после имени – пустые квадратные скобки «[]», указывающие на то, что аргумент представляет из себя массив.

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

double Array[]; // подготовим массив для демонстрации работы нашей будущей функции
int size=21;
ArrayResize(Array,size);

for(int i=0; i<size; i++) // заполним массив числами от 100, до 120
      Array[i]=i+100.0;


   if( RemoveFromArray(Array, 17) )
      for(int i=ArraySize(Array)-1; i>=0; i--) // выведем в журнал все элементы массива
         Print("Array[",i,"]=",Array[i]);

Напишем функцию, которая будет удалять указанный элемент.
//+------------------------------------------------------------------+
//| Удаляет элемент динамического массива с указанным индексом       |
//| и возвращает "true", если удаление прошло удачно, в противном    |
//| случае возвращает false                                          |
//+------------------------------------------------------------------+
bool RemoveFromArray(double &array[], int index)
  {
   int size=ArraySize(array);

   if(index<size) // индекс удаляемого элемента не выходит за пределы массива
     {
      for(int i=index+1; i<size; i++) // сдвинем все элементы за удаляемым влево на один
         array[i-1]=array[i];

      size=ArrayResize(array,size-1); // уменьшим размер массива на единицу, тем самым отсечём ненужный последний элемент

      return(true); // возвратим истину, так как удаление прошло успешно
     }
   else
     {
      Print("Индекс удаляемого элемента находится за пределами массива");
      return(false);// возвратим ложь, так как удаление прошло неудачно
     }
  }

Если по каким-то причинам вы не хотите изменять исходный массив, а все действия собираетесь производить с копией массива, то можно воспользоваться функцией ArrayCopy(), которая копирует содержимое одного массива в другой. Массив-приёмник, естественно, должен быть объявлен и иметь тот же тип, что массив-источник. В связи с тем, что операция копирования занимает какое-то время, старайтесь использовать эту функцию, когда это действительно необходимо в рамках вашей задачи.

Многомерные массивы

Массивы, в которых каждый элемент имеет один индекс, называются одномерными. До этого мы рассматривали только одномерные массивы. Если же каждый элемент массива имеет более одного индекса – то мы имеем дело с многомерным массивом.

Запись вида:

int Numbers[5][4];

объявляет двумерный массив или, другими словами, массив пяти массивов по четыре элемента. Визуально двумерный массив можно представить как таблицу, в данном случае из пяти строк и четырёх столбцов. Элементы двумерного массива можно представить как ячейки таблицы. Строка и столбец, на пересечении которых располагается ячейка – это индексы элемента массива. Например, выражение Numbers[2][1]=48 можно интерпретировать так: в ячейку таблицы на третьей строке во втором столбце записали число «48» (помним, что индексация в каждом измерении массива начинается с нуля).

Так как у каждого элемента массива Numbers два индекса, то перебор всех его значений надо осуществлять с помощью двух циклов, причём один будет вложен в другой. Например, следующий код присваивает всем элементам массива значение «10», кроме элемента Numbers[2][1], которому присваивается значение «48»:

for(int i=0; i<5; i++)
  for(int j=0; j<4; j++)
   {
     if(i==2 && j==1)
      Numbers[i][j]=48;
    else
     Numbers[i][j]=10;
   }

Язык MQL4 поддерживает не более четырёх измерений у массива:

double Prices[8][2][5]    // пример трёхмерного массива
double Levels[4][3][1][6] // четырёхмерный массив

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

double Prices[][2][5]; // правильноe объявление трёхмерного динамического массива
double Prices[][][5]; // неправильное объявление

Отсюда следует, что с помощью функции ArrayResize() можно менять только размер первого измерения массива:

double Prices[][2][5];
int new_size = ArrayResize(Prices,10);

Надо учитывать, что в этом случае функция ArrayResize() вернёт общий размер элементов массива, то есть new_size станет равным 100 (10*2*5).

На практике большинство задач, с которыми вам придётся столкнуться, вряд ли потребует использования массивов, размерность которых превышает 2. Двумерные динамические массивы, например, хорошо подходят для обработки табличных данных, поэтому встречаются намного чаще, чем другие многомерные массивы. Однако иногда требуется, чтобы была возможность изменять размер двумерного массива в обоих измерениях. Например, в процессе выполнения программы нам может понадобиться массив Numbers[Rows][Columns], где Rows и Columns – это переменные, но мы можем задать с помощью переменной только размер первого измерения. Обойти это ограничение можно с помощью простого трюка: свернуть оба измерения массива в одно. То есть объявить одномерный динамический массив и установить его размер равным произведению Rows*Columns, а второе измерение определять с помощью простой формулы row*Columns+column, где row и column – координаты ячейки в «виртуальной таблице», а Columns – это размер второго измерения (количество столбцов).

Для того, чтобы было удобнее использовать одномерный динамический массив в качестве двумерного, напишем «обёртку» в виде структуры, в которой будем хранить и сам массив, и его свойства, а также методы для обращения к элементам массива с помощью двух индексов.

struct int2DArray
  {
private:
   int               rows;                                                                // количество строк
   int               columns;                                                             // количество столбцов
   int               Values[];                                                            // одномерный динамический массив
   int               GetIndex(int row, int col) {return(row*columns+col);};               // возвращает индекс ячейки в массиве
public:
   int               Create(int row, int col);                                            // устанавливает массиву размер, необходимый для хранения ячеек
   int               Get(int row, int col) {return(Values[GetIndex(row,col)]);};          // возвращает содержимое ячейки (row,col)
   void              Set(int row, int col, int value) {Values[GetIndex(row,col)]=value;}; // записывает значение в ячейку (row,col)
   int               AddRows(int amount);                                                 // добавляет строку к двумерному массиву
   int               Rows() {return(rows);};                                              // возвращает число строк в массиве
   int               Columns() {return(columns);};                                        // возвращает число строк в массиве
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int int2DArray::Create(int row, int col)
  {
   int size=row*col;
   if(size<=0)
      return(EMPTY);
   rows=row;
   columns=col;

   ArrayFree(Values);
   ArrayResize(Values,size);
   ArrayInitialize(Values,0);
   return(size);
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int int2DArray::AddRows(int amount)
  {
   int new_size=ArrayResize(Values,ArraySize(Values)+amount*columns);
   rows+=amount;
   return(new_size);
  }
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
// Объявим переменную структуры:
   int2DArray Array;

// Зададим размер массива в 10 строк и 5 столбцов:
   int size=Array.Create(10,5);

// Все ячейки проинициализированы нулевым значением, а их общее количество - пятьдесят:
   Print("Размер массива=",size);

// Заполним ячейки (0;3), (5;0), (8;4) какими-то ненулевыми значениями
   Array.Set(0,3,9);
   Array.Set(5,0,18);
   Array.Set(8,4,3);

// Убедимся в том, что заполнились именно те ячейки, индексы которых мы указали в методе Set:
   for(int i=0; i<Array.Rows(); i++)
      for(int j=0; j<Array.Columns(); j++)
        {
         PrintFormat("Array[%d,%d]=%d",i,j,Array.Get(i,j));
        }

// Добавим 2 строки в "массив". Так как общее число столбцов у нас пять, то физически в конец одномерного массива добавятся 2*5=10 элементов.
   Array.AddRows(2);

// Заполним ячейки (10;3), (11;4) какими-то ненулевыми значениями
   Array.Set(10,3,4);
   Array.Set(11,4,31);

// Убедимся в том, что заполнились именно те ячейки, индексы которых мы указали в методе Set:
   for(int i=0; i<Array.Rows(); i++)
      for(int j=0; j<Array.Columns(); j++)
        {
         PrintFormat("Array[%d,%d]=%d",i,j,Array.Get(i,j));
        }

   return(INIT_SUCCEEDED);
  }

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

«Ошибка на единицу»

Одна из самых распространённых ошибок при работе с циклами for и другими, если они используют счётчик – это так называемая «ошибка неучтённой единицы» или «ошибка на единицу». Возникает она из-за неправильного использования операторов сравнения. Например, цикл

for(int i=1; i<=5; i++)
     Print(i);

выполнится 5 раз, а цикл

for(int i=1; i<5; i++)
  Print(i);

будет выполнен всего 4 раза. Во втором случае при значении 5 переменной i условие i<5 будет ложным, значит, тело цикла при этом значении выполнено не будет. Важно помнить, что цикл выполняется только тогда, когда условие его продолжения истинно. «Ошибка неучтённой единицы» наибольшую опасность представляет в контексте работы с массивами. Так как индексация элементов массива начинается с нуля, то индекс последнего элемента на единицу меньше размера самого массива. Если из-за «неучтённой единицы» цикл итерации по массиву выполнится на один раз больше требуемого, то программа прекратит работу из-за критической ошибки. Произойдёт так называемый «выход за пределы массива»:

int Numbers[];
int size=ArrayResize(Numbers,100);
for(int i=0; i<=size; i++) // необходимо использовать строгое условие i<size 
Print(Numbers[i]);

Здесь переменная size равна размеру массива, то есть 100. На последней итерации цикла счётчик i примет значение 100, но элемента с таким индексом в массиве нет, последний имеет индекс 99. Поэтому условие продолжения цикла должно быть строгим – i<size.

Быстрая сортировка составных данных с помощью массива ссылок

Рассмотрим интересный пример использования массивов для построения индексов в стиле базы данных. В арсенале языка MQL4 имеется функция ArraySort() для сортировки числовых массивов по первому измерению. Отсортировать, скажем, массив структур или объектов по какому-то числовому элементу с помощью функции ArraySort() нельзя. Казалось бы, единственный выход – писать функцию сортировки с физическим обменом местами элементов массива структур. Но можно создать двумерный динамический числовой массив, в который достаточно скопировать нужные значения для сортировки, и индекс элементов, которым принадлежат эти данные в массиве-источнике. Отсортировав такой массив по первому измерению, мы можем потом обращаться к нужному элементу в массиве-источнике через индекс во втором измерении. Физической сортировки массива-источника не произойдёт. Давайте рассмотрим подобную сортировку на конкретном примере. Только мы будем сортировать не какой-то массив структур, а информацию об открытых ордерах, даже не копируя её в массив.

Определим критерии сортировки в виде перечисления:

enum field
  {
   fOrderOpenTime,   // Время открытия
   fOrderType,       // Тип
   fOrderLots,       // Объём
   fOrderOpenPrice,  // Цена открытия
   fOrderStopLoss,   // S/L
   fOrderTakeProfit, // T/P
   fOrderCommission, // Комиссия
   fOrderSwap,       // Своп
   fOrderProfit      // Прибыль
  };

Напишем функцию, которая строит индекс по переданному критерию сортировки:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void BuildIndex(field criterion, int &out[])
  {
   double Index[][2];
   int size=OrdersTotal(); // общее количество ордеров
   ArrayResize(Index,size);

   for(int i=size-1; i>=0; i--) // перебор всех ордеров
      if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES))
        {
         Index[i][1]=OrderTicket();

         switch(criterion)
           {
            case fOrderOpenTime:
               Index[i][0]=double(OrderOpenTime());
               break;
            case fOrderType:
               Index[i][0]=OrderType();
               break;
            case fOrderLots:
               Index[i][0]=OrderLots();
               break;
            case fOrderOpenPrice:
               Index[i][0]=OrderOpenPrice();
               break;
            case fOrderStopLoss:
               Index[i][0]=OrderStopLoss();
               break;
            case fOrderTakeProfit:
               Index[i][0]=OrderTakeProfit();
               break;
            case fOrderCommission:
               Index[i][0]=OrderCommission();
               break;
            case fOrderSwap:
               Index[i][0]=OrderSwap();
               break;
            case fOrderProfit:
               Index[i][0]=OrderProfit();
               break;
           }
        }

   ArraySort(Index);// сортируем по первому измерению
   ArrayResize(out,size);

// копируем тикеты в переданный массив в отсортированном порядке
   for(int i=0; i<size; i++)
      out[i]=int(Index[i][1]);
  }

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  { 
   /* Перед вызовом функции надо объявить массив, в который функция поместит ссылки на ордера в отсортированном порядке. Очевидно, что в качестве таких ссылок будут использованы тикеты,– уникальные идентификаторы ордеров. */
int Tickets[];
/* Вызываем функцию. В качестве критерия указываем прибыль ордера, то есть сортировку будем производить по текущей прибыли ордеров от наименьшей к наибольшей: */
BuildIndex(fOrderProfit, Tickets);
// Выведем в журнал информацию об открытых ордерах, отсортированную по прибыли.
 
// перебираем массив тикетов
for(int i = ArraySize(Tickets) -1; i>=0; i--)
     {
      // выбираем ордер с указанным тикетом
      if(OrderSelect(Tickets[i],SELECT_BY_TICKET))
        {
         int digits=int(SymbolInfoInteger(OrderSymbol(),SYMBOL_DIGITS));
        // формируем и выводим в журнал строку с информацией по выбранному ордеру
         Print(OrderTicket()," | ",
               TimeToString(OrderOpenTime())," | ",
               (OrderType()==OP_BUY)?"buy":"sell"," | ",
               DoubleToStr(OrderLots(),2)," | ",
               OrderSymbol()," | ",
               DoubleToStr(OrderOpenPrice(),digits)," | ",
               DoubleToStr(OrderStopLoss(),digits)," | ",
               DoubleToStr(OrderTakeProfit(),digits)," | ",
               DoubleToStr(OrderCommission(),2)," | ",
               DoubleToStr(OrderSwap(),2)," | ",
               DoubleToStr(OrderProfit(),2));
        }
     }
  return(INIT_SUCCEEDED);
}

Фрагмент журнала с результатами работы:

Крайнее правое число после разделителя – это текущая прибыль ордера. Как видите, строки отсортированы именно по этому значению.

Заключение

В этом уроке мы рассмотрели использование циклов при работе с массивами. Заодно вспомнили и закрепили основные сведения о самих массивах и циклах, изучили особенности их реализации в языке MQL4, разобрали типичные примеры и приёмы программирования массивов и циклов.

Тема на форуме

С уважением, Юрий Лосев aka lsv107
Tlap.com

Уроки по MQL4 , , ,