Инициализация

При описании переменных существует возможность задать начальное значение — оно указывается после имени переменной и символа '=', и должно соответствовать типу переменной или приводиться к нему (о приведении типов — в соответствующем разделе).

int i = 3jk = 10;

Здесь i и k проинициализированы явно, а j — нет.

В качестве начального значения можно указать не только константу (литерал соответствующего типа), но и выражение — своего рода формулу для расчета. Про выражения мы подробно поговорим отдельно. Пока лишь приведем простой пример:

int i = 3j = ik = i + j;

Здесь переменная j получает такое же значение, что и переменная i, а переменная k — сумму i и j. Строго говоря, во всех трех случаях мы здесь видим выражения, но константа (3) — это особый вырожденный вариант выражения. Во втором случае выражением является единственное имя переменной, т.е. результатом выражения будет значение этой переменной без преобразований. В третьем случае в выражении происходит обращение к двум переменным i и j, и над их значениями выполняется операция сложения, после чего результат попадает в переменную k.

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

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

int i = 3j = i;
int k = i + j;

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

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

Обычно предполагается, что в локальные переменные без инициализации будет произведена запись рабочих значений когда-то позднее в коде алгоритма, например, с помощью операции присваивания, о которой будет рассказано далее. Она похожа по синтаксису на инициализацию, так как тоже использует знак равно '=' для переноса значения из той "конструкции", что стоит справа (это может быть константа, переменная, выражение, вызов функции), в переменную, которая упомянута слева. Слева от '=' может находиться только переменная.

Программист должен следить за тем, чтобы чтение из неинициализированной переменной происходило только после присвоения ей осмысленного значения. Компилятор выдает предупреждение, если это не так ("возможное использование неинициализированной переменной" — "possible use of uninitialized variable").

С глобальными переменными все иначе.

Примером глобальной переменной является входной параметр GreetingHour скрипта GoodTime2 из первой Части. То, что переменная была описана с ключевым словом input, не влияет на её прочие свойства как переменной. Мы могли бы исключить её инициализацию и описать следующим образом:

input uint GreetingHour;

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

Вне зависимости от типа переменной неявная инициализация всегда производится значением, эквивалентным нулю. Например, переменной типа bool будет по умолчанию установлено значение false, а переменной типа datetime — D'1970.01.01 00:00:00'. Для строк существует специальное значение NULL. Если хотите, это "еще более пустая строка", чем пустые кавычки "", потому что под них всё же выделяется память, куда в самое начало помещается единственный терминальный нулевой символ.

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

Создадим новый скрипт VariableScopes.mq5 с примерами описания локальных и глобальных переменных (MQL5/Scripts/MQL5Book/VariableScopes.mq5).

// global variables
int ijk;    // all are 0s
int m = 1;      // m = 1                (place breakpoint on this line)
int n = i + m;  // n = 1
void OnStart()
{
  // local variables
  int xyz;
  int k = m// warning: declaration of 'k' hides global variable
  int j = j// warning: declaration of 'j' hides global variable
  // use variables in assignment instructions  
  x = n;     // ok, 1
  z = y;     // warning: possible use of uninitialized variable 'y'
  j = 10;    // change local j, global j is still 0
}
// compilation error
// int bad = x; // 'x' - undeclared identifier

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

Здесь глобальными являются переменные i, j, k, m, n, т.к. они описаны вне функции (в данном случае у нас только одна функция — обязательная для скриптов функция OnStart). i, j, k неявно получают значение 0. m и n содержат 1.

Вы можете запустить скрипт в режиме отладки в пошаговом режиме и убедиться, что значения переменных меняются именно так. Для этого нужно предварительно установить точку остановки на строку с инициализацией одной из глобальных переменных, например m. Поставьте текстовый курсор на эту строку и выполните команду Отладка -> Переключить точку останова (F9), и строка будет подсвечена в левом поле синим значком, сигнализирующим, что здесь выполнение программы остановится, если она будет работать под отладчиком.

Далее следует, собственно, запустить программу под отладчиком, для чего выполните команду Отладка -> Начать на реальных данных (F5). В терминале в этот момент открывается новый график, на котором начинает выполняться данный скрипт (в правом верхнем углу — надпись "VariableScopes (Отладка)"), но тут же приостанавливается, и мы попадаем обратно в MetaEditor. В нем мы должны увидеть примерно следующую картину.

Пошаговая отладка и просмотр переменных в MetaEditor

Пошаговая отладка и просмотр переменных в MetaEditor

Строка с точкой остановки теперь помечена значком со стрелочкой — это текущая инструкция, которую программа готовится выполнить, но еще не выполнила. Внизу слева показан текущий стек программы, состоящий пока только из одной записи: @global_initializations. Внизу справа можно ввести выражения для наблюдения за их значениями в реальном времени. Нас интересуют значения переменных, так что введем последовательно i, j, k, m, n, x, y, z (каждую в отдельной строке).

В дальнейшем вы увидите, что MetaEditor автоматически добавляет для просмотра переменные из текущего контекста (например, все локальные переменные и входные параметры функции, когда выполняются инструкции внутри функции). Но сейчас мы добавим x, y, z вручную и заранее, просто чтобы показать, что они не определены вне функции.

Обратите внимание, что для локальных переменных вместо значения написано "Неизвестный идентификатор" ("Unknown identifier"), потому что блок функции OnStart, где они находятся, еще не существует. Глобальные переменные i и j будут иметь сначала нулевые значения. Глобальная переменная k нигде не используется и потому исключена компилятором.

Если выполнить один шаг исполнения программы (выполнить инструкцию на текущей строке кода) с помощью команды Шаг с заходом (F11) или Шаг с обходом (F10), мы увидим как переменная m получит значение 1. Еще один шаг продолжит инициализацию уже для переменной n, и она тоже станет равной 1.

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

Инициализация локальных переменных происходит в тот момент, когда выполнение инструкций программы приходит в блок кода, где они определены. Поэтому переменные x, y, z создаются только после захода в функцию OnStart.

Когда отладчик окажется внутри функции OnStart, Вы сможете увидеть, если повезет, что в x, y и z действительно находятся изначально случайные значения. "Везение" здесь заключается в том, что эти случайные значения могут вполне оказаться нулевыми, и тогда их нельзя будет отличить от неявной инициализации нулем, которую компилятор делает для глобальных переменных. Если повторить запуск скрипта несколько раз, "мусор" в локальных переменных, скорее всего, будет разным и более наглядным. Они не инициализированы явно и потому их содержимое может оказаться любым.

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

Пошаговая отладка и просмотр переменных в MetaEditor (строка 23)

Пошаговая отладка и просмотр переменных в MetaEditor (строка 23)

Пошаговая отладка и просмотр переменных в MetaEditor (строка 24)

Пошаговая отладка и просмотр переменных в MetaEditor (строка 24)

В коде далее демонстрируется, как эти переменные могли бы использоваться простейшим образом в операторах присваивания. Значение глобальной переменной n копируется в локальную x без проблем, потому что n была проиницилизирована. Однако в строке, где содержимое переменной y копируется в переменную z, возникает предупреждение компилятора, так как y является локальной, и в неё к этому моменту еще ничего не было записано (явной инициализации нет, как и прочих операторов, способных установить её значение).

Внутри функции допускается описывать переменные с теми же именами, которые уже использовались для глобальных переменных. Аналогичная ситуация может возникать во вложенных локальных блоках, если во внутреннем блоке создается переменная с именем, существующем во внешнем блоке. Однако такая практика не рекомендуется, поскольку может приводить к логическим ошибкам. Компилятор выдает в таких случаях предупреждения ("определение скрывает глобальную/локальную переменную" — "declaration hides global/local variable").

В результате подобного переопределения локальная переменная (как переменная k в примере выше) перекрывает внутри функции одноименную глобальную. Это две разных переменных, несмотря на одно и то же имя. Локальная k известна внутри OnStart, а глобальная k — везде кроме OnStart. Иными словами, любые операции с переменной k внутри блока будут влиять только на локальную переменную. Поэтому после выхода из функции OnStart (если бы это не была единственная и главная функция скрипта), мы обнаружили бы, что глобальная переменная k по-прежнему равна нулю.

Локальная переменная j не только перекрывает глобальную переменную j, но и инициализируется значением последней. В строке с описанием j внутри OnStart локальная версия j еще находится в процессе создания, когда начальное значение для неё считывается из глобальной версии j. После успешного определения локальной j, это имя перекрывает глобальную версию, и последующие изменения переменной j относятся именно к локальной версии.

В конце исходного кода закомментирована попытка объявить еще одну глобальную переменную — bad, в инициализации которой происходит обращение к значению переменной x. Эта строка вызывает ошибку компилятора, так как переменная x неизвестна вне функции OnStart, где она определена.