Inicialización y medición de cadenas

Como sabemos por la sección Tipo de cadena, basta con describir en el código una variable de tipo string, y estará listo para funcionar.

Para cualquier variable de tipo string se asignan 12 bytes para la estructura de servicio que es la representación interna de la cadena. La estructura contiene la dirección de memoria (puntero) donde se almacena el texto, junto con alguna otra meta-información. El texto en sí también requiere memoria suficiente, pero este búfer se asigna con algunas optimizaciones menos obvias.

En particular, podemos describir una cadena junto con una inicialización explícita, incluyendo un literal vacío:

string s = ""// pointer to the literal containing '\0'

En ese caso, el puntero se establecerá directamente en el literal, y no se asignará memoria para el búfer (aunque el literal sea largo). Obviamente, ya se ha asignado memoria estática para el literal, y puede utilizarse de forma directa. La memoria para el búfer se asignará sólo si alguna instrucción del programa cambia el contenido de la línea. Por ejemplo (observe que la operación de suma '+' está permitida para las cadenas):

int n = 1;
s += (string)n;    // pointer to memory containing "1"'\0'[plus reserve]

A partir de este momento, la cadena contiene en realidad el texto «1» y, en sentido estricto, requiere memoria para dos caracteres: el dígito «1» y el cero terminal implícito '\0' (terminador de cadena). No obstante, el sistema asignará un búfer mayor, con algo de espacio reservado.

Cuando declaramos una variable sin valor inicial, el compilador la inicializa implícitamente, aunque en este caso con un valor especial NULL:

string z// memory for the pointer is not allocated, pointer = NULL

Una cadena de este tipo sólo requiere 12 bytes por estructura, y el puntero no apunta a ninguna parte: eso es lo que significa NULL.

En futuras versiones del compilador MQL5, este comportamiento puede cambiar, y siempre se asignará inicialmente una pequeña área de memoria para una cadena vacía, proporcionando algo de espacio reservado.

Además de estas características internas, las variables del tipo string no difieren de las variables de otros tipos. Sin embargo, debido a que las cadenas pueden tener una longitud variable y, lo que es más importante, pueden cambiar de longitud durante el algoritmo, esto puede afectar negativamente a la eficiencia de la asignación de memoria y al rendimiento.

Por ejemplo, si en algún momento el programa necesita añadir una nueva palabra a una cadena, puede resultar que no haya suficiente memoria asignada a la cadena. El entorno de ejecución del programa MQL, de forma imperceptible para el usuario, encontrará un nuevo bloque de memoria libre de mayor tamaño y copiará el valor antiguo junto con la palabra añadida. Después, la dirección antigua se sustituye por otra nueva en la estructura de servicios de la línea.

Si hay muchas operaciones de este tipo, la ralentización debida a la copia puede llegar a ser notable y, además, la memoria del programa está sujeta a fragmentación: las pequeñas áreas de memoria antiguas liberadas tras la copia forman vacíos que no son adecuados en tamaño para cadenas grandes y, por lo tanto, conducen al desperdicio de memoria. Por supuesto, el terminal es capaz de controlar estas situaciones y reorganizar la memoria, pero esto también tiene un coste.

La forma más eficaz de resolver este problema es indicar explícitamente por adelantado el tamaño del búfer para la cadena e inicializarlo utilizando las funciones integradas de la API de MQL5, que veremos más adelante en esta sección.

La base de esta optimización es simplemente que el tamaño de la memoria asignada puede exceder la longitud actual (y, potencialmente, la futura) de la cadena, que viene determinada por el primer carácter nulo del texto. Así, podemos asignar un búfer para 100 caracteres, pero desde el principio poner '\0' al principio, lo que dará una cadena de longitud cero ("").

Naturalmente, se supone que en estos casos el programador puede calcular de forma aproximada y de antemano la longitud prevista de la cadena o su tasa de crecimiento.

Dado que las cadenas en MQL5 se basan en caracteres de doble byte (lo que garantiza la compatibilidad Unicode), el tamaño de la cadena y del búfer en caracteres debe multiplicarse por 2 para obtener la cantidad de memoria ocupada y asignada en bytes.

Al final de la sección se ofrecerá un ejemplo general de utilización de todas las funciones (StringInit.mq5).

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

La función StringInit se utiliza para inicializar (asignar y llenar memoria) y desinicializar (liberar memoria) cadenas. La variable que se va a procesar se pasa en el primer parámetro.

Si el parámetro capacity es mayor que 0, se asigna un búfer (área de memoria) del tamaño especificado para la cadena y se rellena con el símbolo character. Si character es 0, la longitud de la cadena será cero, porque el primer carácter es terminal.

Si el parámetro capacity es 0, se libera la memoria previamente asignada. El estado de la variable pasa a ser idéntico al que tenía si se acababa de declarar sin inicializar (el puntero al búfer es NULL). Más sencillamente, se puede hacer lo mismo poniendo la variable cadena en NULL.

La función devuelve un indicador de éxito (true) o errores (false).

bool StringReserve(string &variable, uint capacity)

La función StringReserve aumenta o disminuye el tamaño del búfer de la cadena variable, al menos hasta el número de caracteres especificado en el parámetro capacity. Si el valor de capacity es menor que la longitud actual de la cadena, la función no hace nada. De hecho, el tamaño del búfer puede ser mayor que el solicitado: el entorno hace esto por razones de eficiencia en futuras manipulaciones con la cadena. Así, si se llama a la función con un valor reducido para el búfer, puede ignorar la petición y seguir devolviendo true («sin errores»).

El tamaño actual del búfer puede obtenerse mediante la función StringBufferLen (véase más abajo).

En caso de éxito, la función devuelve true, en caso contrario – false.

A diferencia de StringInit, la función StringReserve no modifica el contenido de la cadena y no la rellena con caracteres.

bool StringFill(string &variable, ushort character)

La función StringFill rellena la cadena variable especificada con el carácter character en toda su longitud actual (hasta el primer cero). Si se asigna un búfer a una cadena, la modificación se realiza in situ, sin operaciones intermedias de nueva línea y copia.

La función devuelve un indicador de éxito (true) o errores (false).

int StringBufferLen(const string &variable)

La función devuelve el tamaño del búfer asignado a la cadena variable.

Tenga en cuenta que, para una cadena inicializada con un literal, no se asigna inicialmente ningún búfer porque el puntero apunta al literal. Por lo tanto, la función devolverá 0 aunque la longitud de la cadena StringLen (véase más abajo) pueda ser mayor.

El valor -1 significa que la línea pertenece al terminal cliente y no puede modificarse.

bool StringSetLength(string &variable, uint length)

La función establece la longitud especificada en caracteres length para la cadena variable. El valor de length no debe ser mayor que la longitud actual de la cadena. En otras palabras, la función sólo permite acortar la cadena, pero no alargarla. La longitud de la cadena se incrementa automáticamente cuando se llama a la función StringAdd o se realiza la operación de suma '+'.

El equivalente de la función StringSetLength es la llamada StringSetCharacter(variable, length, 0) (véase la sección Trabajar con símbolos y páginas de códigos).

Si ya se ha asignado un búfer para la cadena antes de la llamada a la función, ésta no lo modifica. Si la cadena no tenía un búfer (apuntaba a un literal), al disminuir la longitud se asigna un nuevo búfer y se copia en él la cadena acortada.

La función devuelve true o false en caso de éxito o fracaso, respectivamente.

int StringLen(const string text)

La función devuelve el número de caracteres de la cadena text. El terminal cero no se tiene en cuenta.

Tenga en cuenta que el parámetro se pasa por valor, por lo que puede calcular la longitud de las cadenas no sólo en variables, sino también para cualquier otro valor intermedio: literales o resultados de cálculo.

El script StringInit.mq5 se ha creado para demostrar las funciones anteriores. Utiliza una versión especial de la macro PRT, PRTE, que analiza el resultado de una expresión en true o false y, en el caso de esta última, devuelve además un código de error:

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

Para la salida de depuración al registro de una cadena y sus métricas actuales (longitud de línea y tamaño del búfer), se implementa la función StrOut:

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

Utiliza las funciones integradas StringLen y StringBufferLen.

El script de prueba realiza una serie de acciones sobre una cadena en OnStart:

void OnStart()
{
   string s = "message";
   StrOut(s);
   PRTE(StringReserve(s100)); // ok, but we get a buffer larger than requested: 260
   StrOut(s);
   PRTE(StringReserve(s500)); // ok, buffer is increased to 500
   StrOut(s);
   PRTE(StringSetLength(s4)); // ok: string is shortened
   StrOut(s);
   s += "age";
   PRTE(StringReserve(s100)); // ok: buffer remains at 500
   StrOut(s);
   PRTE(StringSetLength(s8)); // no: string lengthening is not supported
   StrOut(s);                   //     via StringSetLength
   PRTE(StringInit(s8, '$')); // ok: line increased by padding
   StrOut(s);                   //     buffer remains the same
   PRTE(StringFill(s0));      // ok: string collapsed to empty because
   StrOut(s);                   //     was filled with 0s, the buffer is the same
   PRTE(StringInit(s0));      // ok: line is zeroed, including buffer
                                // we could just write s = NULL;
   StrOut(s);
}

El script registrará los siguientes mensajes:

'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

Tenga en cuenta que la llamada StringSetLength con longitud de cadena aumentada terminó con el error 5035 (ERR_STRING_SMALL_LEN).