Инициализация и измерение строк

Как мы знаем из раздела о строковом типе, достаточно описать в коде переменную типа string, и она будет готова к работе.

Под любую переменную типа string выделяется 12 байтов на служебную структуру — внутреннее представление строки. Структура содержит адрес памяти (указатель), где хранится текст, и кое-какую другую мета-информацию. Сам текст также требует памяти достаточного размера, но выделяется этот буфер с применением некоторых не столь очевидных оптимизаций.

В частности, мы можем описать строку вместе с явной инициализацией, в том числе и пустым литералом:

string s = ""// указатель на литерал, содержащий '\0'

В таком случае указатель установится непосредственно на литерал, и память под буфер не выделяется (даже если литерал — длинный). Очевидно, что под литерал уже выделена статическая память и её можно использовать напрямую. Память под буфер будет распределена, только если какая-либо инструкция в программе изменит содержимое строки. Например (напомним, что для строк определена операция сложения '+'):

int n = 1;
s += (string)n;    // указатель на память, содержащую "1"'\0'[плюс резерв]

С этого момента строка фактически содержит текст "1" и требует, строго говоря, памяти для двух символов: цифры "1" и неявного терминального нуля '\0' (признака конца строки). Однако система выделит буфер большего размера, с запасом.

Когда мы описываем переменную без начального значения, она все равно неявно инициализируется компилятором, правда в этом случае — специальным значением NULL:

string z// память для указателя не выделена, указатель = NULL

Такая строка требует только 12 байтов на структуру, а указатель никуда не ведет: это и обозначает NULL.

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

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

Например, если в какой-то момент программе потребуется добавить в строку новое слово, то может оказаться, что выделенной под строку памяти недостаточно. Тогда среда исполнения MQL-программы незаметно для пользователя найдет новый свободный блок памяти увеличенного размера и скопирует туда прежнее значение вместе с добавляемым словом. После этого старый адрес подменяется новым в служебной структуре строки.

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

Наиболее действенным способом решения данной проблемы является явное заблаговременное указание размера буфера под строку и её инициализация с помощью встроенных функций MQL5 API, которые мы рассмотрим далее.

Основой для данной оптимизации как раз служит то, что размер выделенной памяти может превышать текущую (и потенциальную будущую) длину строки, которая определяется по первому нулевому символу в тексте. Таким образом, мы можем распределить буфер под 100 символов, но изначально поставить в самое начало '\0', что даст строку нулевой длины ("").

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

Поскольку строки в MQL5 строятся на двухбайтовых символах (что обеспечивает поддержку Unicode), размер строки и буфера в символах следует умножать на 2, чтобы получить объем занимаемой и выделенной памяти в байтах.

Общий пример использования всех функций (StringInit.mq5) будет приведен в конце раздела.

bool StringInit(string &variable, int capacity = 0, ushort character = 0)

Функция StringInit применяется дли инициализации (распределения и заполнения памяти) и деинициализации (освобождения памяти) строк. Обрабатываемая переменная передается в первом параметре.

Если параметр capacity больше 0, то под строку выделяется буфер (область памяти) указанного размера и заполняется символом character. Если character равен 0, то длина строки будет нулевой, потому что первый же символ является терминальным.

Если параметр capacity равен 0, ранее выделенная память освобождается. Состояние переменной становится идентично тому, как будто её только что описали без инициализации (указатель на буфер равен NULL). Более просто то же самое можно сделать путем присваивания строковой переменной значения NULL.

Функция возвращает признак успеха (true) или ошибки (false).

bool StringReserve(string &variable, uint capacity)

Функция StringReserve увеличивает или уменьшает размер буфера строки variable как минимум до количества символов, указанных в параметре capacity. Если значение capacity меньше текущей длины строки, функция ничего не делает. По факту размер буфера может оказаться больше, чем запрошенный: среда делает так из соображений эффективности будущих манипуляций со строкой. Таким образом, если функция вызвана с уменьшенным значением для буфера, она может проигнорировать запрос и при этом вернёт true ("нет ошибок").

Актуальный размер буфера можно получить с помощью функции StringBufferLen (см. ниже).

В случае успеха функция возвращает true, иначе — false.

В отличие от StringInit функция StringReserve не изменяет содержимое строки и не заполняет её символами.

bool StringFill(string &variable, ushort character)

Функция StringFill заполняет указанную строку variable символом character, на всю её текущую длину (до первого нуля). Если для строки выделен буфер, модификация производится по месту, без промежуточных операций создания новой строки и копирования.

Функция возвращает признак успеха (true) или ошибки (false).

int StringBufferLen(const string &variable)

Функция возвращает размер буфера, распределенного для строки variable.

Обратите внимание, что для строки с инициализацией литералом буфер изначально не выделяется, поскольку указатель ведет на литерал. Поэтому функция вернет 0, хотя длина строки StringLen (см. ниже) может быть и больше.

Значение -1 означает, что строка принадлежит клиентскому терминалу и изменять её нельзя.

bool StringSetLength(string &variable, uint length)

Устанавливает для строки variable указанную длину в символах length. Значение length должно быть не больше текущей длины строки. Иными словами, функция позволяет только укоротить строку, но не удлинить. Увеличение длины строки происходит автоматически при вызове функции StringAdd или выполнении операции сложения '+'.

Эквивалентом функции StringSetLength является вызов: StringSetCharacter(variable, length, 0) (см. раздел Работа с символами и кодовыми страницами).

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

Функция возвращает true или false в случае, соответственно, успеха или ошибки.

int StringLen(const string text)

Функция возвращает число символов в строке text. Терминальный ноль не учитывается.

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

Для демонстрации вышеописанных функций создан скрипт StringInit.mq5. В нем используется особая версия макроса PRT — PRTE, которая анализирует результат выражения на true или false, и в последнем случае дополнительно выводит код ошибки:

#define PRTE(APrint(#A"=", (A) ? "true" : "false:" + (string)GetLastError())

Для отладочного вывода в журнал строки и её текущих метрик (длина строки и размер буфера) реализована функция StrOut:

void StrOut(const string &s)
{
   Print("'"s"' ["StringLen(s), "] "StringBufferLen(s));
}

Она использует встроенные функции StringLen и StringBufferLen.

Тестовый скрипт выполняет ряд действий над строкой в OnStart:

void OnStart()
{
   string s = "message";
   StrOut(s);
   PRTE(StringReserve(s100)); // ok, но получим буфер больше запрошенного: 260
   StrOut(s);
   PRTE(StringReserve(s500)); // ok, буфер увеличен до 500
   StrOut(s);
   PRTE(StringSetLength(s4)); // ok: строка укорочена
   StrOut(s);
   s += "age";
   PRTE(StringReserve(s100)); // ok: буфер остался равным 500
   StrOut(s);
   PRTE(StringSetLength(s8)); // no: удлиннение строк не поддерживается
   StrOut(s);                   //     через StringSetLength
   PRTE(StringInit(s8, '$')); // ok: строка увеличена за счет заполнения
   StrOut(s);                   //     буфер остался прежним
   PRTE(StringFill(s0));      // ok: строка схлопнулась до пустой, потому что
   StrOut(s);                   //     была заполнена 0-ми, буфер прежний
   PRTE(StringInit(s0));      // ok: строка обнулена, включая буфер
                                // можно было написать просто s = NULL;
   StrOut(s);
}

Скрипт выведет в журнал следующие сообщения:

'message' [7] 0
StringReserve(s,100)=true
'message' [7] 260
StringReserve(s,500)=true
'message' [7] 500
StringSetLength(s,4)=true
'mess' [4] 500
StringReserve(s,10)=true
'message' [7] 500
StringSetLength(s,8)=false:5035
'message' [7] 500
StringInit(s,8,'$')=true
'$$$$$$$$' [8] 500
StringFill(s,0)=true
'' [0] 500
StringInit(s,0)=true
'' [0] 0

Обратите внимание, что вызов StringSetLength с увеличенной длиной строки закончился ошибкой 5035 (ERR_STRING_SMALL_LEN).