Logo

Marco Cantù
Pascal Esencial

Capítulo 7 Actualizar
Manejo de cadenas

El manejo de cadenas de caracteres en Delphi es bastante sencillo, pero entre bastidores la situación es bastante compleja. Pascal sigue la forma tradicional de manejar cadenas, Windows tiene su propia manera de hacerlo, prestada del lenguaje C, y las versiones de 32 bits de Delphi incluyen un potente tipo de datos de cadena larga, que constituye el tipo de cadena por defecto en ese lenguaje.

Tipos de cadenas (strings)

En el Turbo Pascal de Borland y en el Delphi de 16 bits, el tipo de cadena típico es una sucesión de caracteres con un octeto (byte) al principio de aquella. Como la longitud se expresa en un solo byte, no puede exceder de 255 caracteres; pero este valor tan bajo crea muchos problemas a la hora de manipular cadenas. Cada cadena viene definida por un tamaño fijo (que, por defecto, es el máximo, 255), aunque puede declarar cadenas más cortas, para ahorrar espacio de memoria.

Un tipo cadena es muy similar a un tipo vector. De hecho, una cadena es casi un vector formado por caracteres. Esto se muestra en el hecho de que puede acceder a una cadena específica usando la notación [].

Para superar los límites de las cadenas en Pascal tradicionales, las versiones de Delphi en 32 bits permiten la existencia de cadenas largas. Hay, de hecho, tres tipos de cadenas:


Uso de cadenas largas

Si sólo usamos el tipo de datos cadenas, obtenemos bien cadenas cortas o cadenas ANSI, dependiendo del valor de la directiva $H del compilador. $H+ (la opción por defecto) representa cadenas largas (el tipo ANSIString), que es lo que usan las componentes de la biblioteca Delphi.

Las cadenas largas en Delphi se basan en un mecanismo de contado de referencias, que contabiliza cuántas variables acceden a una misma cadena en la memoria. Este contado de referencias se usa también para liberar memoria cuando una cadena ya no se usa - esto es, cuando el contador de referencia alcanza el valor cero.

Si desea incrementar el tamaño de una cadena en la memoria pero hay otra cosa en la memoria adyacente, la cadena no puede crecer en esa posición, y por tanto debe hacerse una copia completa de la cadena en otro emplazamiento. Cuando ocurre esta situación, los algoritmos de ejecución de Delphi reasignan la variable por nosotros, de forma totalmente transparente. Simplemente establezca el tamaño de la cadena mediante el procedimiento SetLength, adjudicando de forma efectiva la cantidad requerida de memoria:

SetLength (String1, 200);

En realidad, el procedimiento SetLength realiza una petición de memoria, no una asignación directa de memoria. Reserva la cantidad de memoria requerida para usarla después, sin usar la memoria de hecho. Esta técnica se basa en una característica de los sistemas operativos Windows y es usado por Delphi en todas las asignaciones dinámicas de memoria. Por ejemplo, cuando establece un vector muy grande, su memoria se reserva, pero no se adjudica.

Fijar la longitud de una cadena es raramente necesario. El único caso en que se debe asignar memoria a cadenas largas mediante SetLength es cuando hay que utilizarlas como parámetros para una función API (usando el typecast o modelado correspondiente), como le mostraré en breve.

Un vistazo a las cadenas en memoria

Para ayudarle a entender mejor los detalles de la gestión de memoria para las cadenas, he escrito un sencillo ejemplo StrRef. En este programa declaro dos cadenas globales: Str1 y Str2. Cuando se pulsa el primero de los dos botones, el programa asigna una cadena constante a la primera de las variables, y entonces asigna la segunda variable a la primera.

Str1 := 'Hello';
Str2 := Str1;

Aparte de trabajar con las cadenas, el programa muestra su estado interno en una casilla de lista, usando la siguiente función StringStatus :

function StringStatus (const Str: string): string;
begin
  Result := 'Address: ' + IntToStr (Integer (Str)) +
    ', Length: ' + IntToStr (Length (Str)) + 
    ', References: ' + IntToStr (PInteger (Integer (Str) - 8)^) +
    ', Value: ' + Str;
end;

Es de importancia vital en la función StringStatus transmitir el parámetro de cadena como const. Transmitir este parámetro mediante copiado tendrá el efecto secundario de hacer una referencia adicional a la cadena mientras la función se ejecute. Por el contrario, transmitir el parámetro mediante un parámetro de referencia (var) o constante (const) no implica ulterior referencia a la cadena. En este caso usé un parámetro const, ya que no se espera de la función que modifique la cadena.

Para obtener la dirección de memoria de la cadena (útil para determinar su identidad actual y ver cuándo dos cadenas diferentes se refieren a la misma área de memoria), hice un moldeado (typecast) del tipo cadena al tipo entero, sin más. Las cadenas son, en la práctica, referencias, son punteros. Su valor contiene la posición en la memoria de la cadena.

Para extraer el contador de referencias, he basado el código en el hecho poco conocido de que el contador de la longitud y de referencias son, en realidad, almacenados en la cadena, antes del texto que la compone, y antes de la posición a que apunta la variable de cadena. El offset (negativo) es -4 para la longitud de una cadena (un valor que puede averiguarse más fácilmente usando la función Length), y -8 para el contador de referencias.

Tenga presente que esta información interna sobre offsets puede llegar a cambiar en futuras versiones de Delphi; tampoco hay garantía de que características similares que no aparecen en la documentación sigan disponibles en el futuro.

Ejecutando este ejemplo, debería obtener dos cadenas con el mismo contenigo, la misma posición de memoria, y un contador de referencias de valor 2, como se muestra en la parte superior de la casilla de lista, en la Figura 2.1. Ahora, si cambia el valor de una de las cadenas (no importa cuál), la posición de memoria de la cadena actualizada cambiará. Este es el efecto de la técnica de copia por escritura (copy-on-write).

Figura 7.1: El ejemplo StrRef muestra el estado interno de dos cadenas, incluyendo el contador de referencia actual.

Podemos, de hecho, producir este efecto, mostrado en la segunda parte de la casilla de lista de la Figura 7.1, escribiendo el siguiente código para el gestor del evento OnClick del segundo botón :

procedure TFormStrRef.BtnChangeClick(Sender: TObject);
begin
  Str1 [2] := 'a';
  ListBox1.Items.Add ('Str1 [2] := ''a''');
  ListBox1.Items.Add ('Str1 - ' + StringStatus (Str1));
  ListBox1.Items.Add ('Str2 - ' + StringStatus (Str2));
end;

Observe que el código del método BtnChangeClick puede ser ejecutado sólo después del método BtnAssignClick. Para reforzar esto, el programa comienza con el segundo botón desactivado (su propiedad Enabled se establece como False); activa el botón al final del primer método. Puede usted extender este ejemplo y usar la función StringStatus para explorar el comportamiento de cadenas largas en muchas otras circunstancias.

Cadenas Delphi y el PChars de Windows

Otro punto importante a favor del uso de cadenas largas es que son de terminación nula. Esto significa que son totalmente compatibles con las cadenas de terminación nula copiadas por Windows del lenguaje C. Una cadena de terminación nula es una sucesión de caracteres seguida por un byte a que se da valor cero (o nulo). Esto puede expresarse en Delphi usando un vector de caracteres comenzado en cero, como suele hacerse para implementar cadenas en el lenguaje C. Esta es la razón por la que los vectores de caracteres de terminación nula son tan comunes en las funciones API de Windows (basadas en el lenguaje C). Como las cadenas largas de Pascal son totalmente compatibles con las cadenas de terminación nula en C, se puede, sencillamente, usar cadenas largas y moldearlas (typecast) a PChar cuando sea necesario transmitir una cadena a una función API de Windows.

Por ejemplo, para copiar el título de un formulario a una cadena PChar (usando la función API GetWindowText) y luego copiarla a la leyenda de un botón, se puede escribir el siguiente código :

procedure TForm1.Button1Click (Sender: TObject);
var
  S1: String;
begin
  SetLength (S1, 100);
  GetWindowText (Handle, PChar (S1), Length (S1));
  Button1.Caption := S1;
end;

Encontrará este código en el ejemplo LongStr. Observe que si escribe este código pero no asigna la memoria a la cadena con SetLength, el programa probablemente se derrumbará. Si usa PChar para transmitir un valor (y no para recibir un valor, como en el ejemplo de arriba), el código es aún más simple, porque no hay necesidad de definir una cadena temporal y de inicializarla. La siguiente línea de código transmite la propiedad Caption de una etiqueta como parámtro a una función API, sencillamente moldeándola a PChar:

SetWindowText (Handle, PChar (Label1.Caption));

Cuando necesite moldear una cadena WideString a un tipo compatible Windows, deberá usar PWideChar en vez de PChar para dicha conversión. Wide strings se usan a menudo para programas OLE y COM.

Habiendo presentado la parte agradeable de este asunto, ahora quiero centrarme en los defectos. Hay algunos problemas que podrían aparecer al convertir una cadena larga en un PChar. Esencialmente, el problema subyacente es que tras esta conversión usted se hacer responsable de la cadena y su contenido, y Delphi no le ayudará más en ello. Observe el siguiente pequeño cambio hecho al primer fragmento de código que aparece arriba, Button1Click:

procedure TForm1.Button2Click(Sender: TObject);
var
  S1: String;
begin
  SetLength (S1, 100);
  GetWindowText (Handle, PChar (S1), Length (S1));
  S1 := S1 + ' is the title'; // esto no funciona
  Button1.Caption := S1;
end;

Este programa se compila, pero cuando lo ejecute, se llevará una sorpresa: la leyenda del botón llevará el texto original del título de la ventana, sin el texto de la cadena constante que se le añadió. El problema consiste en que cuando Windows escribe en la cadena (dentro de la llamada a la API GetWindowText), no establece adecuadamente la longitud de la cadena larga Pascal. Delphi aún puede usar esta cadena como salida y hacerse una idea de dónde finaliza, pudiendo buscar la terminación nula; pero si usted le añade más caracteres tras la terminación nula, serán ignorados.

¿ Cómo podemos resolver este problema ? La solución pasa por decirle al sistema que convierta la cadena devuelta por la llamada a la API GetWindowText, en una cadena tipo Pascal. Sin embargo, si escribe el siguiente código ...

S1 := String (S1);

... el sistema lo ignorará, porque convertir un tipo de datos en sí mismo es una operación inútil. Para obtener la cadena larga tipo Pascal adecuada, tendrá que remoldear la cadena a un PChar y dejar que Delphi la vuelva a convertir en una cadena :

S1 := String (PChar (S1));

De hecho, puede ahorrarse la conversión a cadena, porque las conversiones de PChar a cadena son automáticas en Delphi. Esta es la versión final del código :

procedure TForm1.Button3Click(Sender: TObject);
var
  S1: String;
begin
  SetLength (S1, 100);
  GetWindowText (Handle, PChar (S1), Length (S1));
  S1 := String (PChar (S1));
  S1 := S1 + ' is the title';
  Button3.Caption := S1;
end;

Una alternativa es restablecer la longitud de la cadena tipo Delphi, usando la longitud de la cadena PChar, escribiendo :

SetLength (S1, StrLen (PChar (S1)));

Encontrará tres versiones de este código en el ejemplo LongStr, que tiene tres botones para ser ejecutado. De cualquier forma, si sólo tiene que acceder al título de un formulario, puede simplemente utilizar la propiedad Caption del objeto formulario mismo. No es necesario escribir todo este código, que lleva a confusión. Sólo lo hemos mostrado para ilustrar los problemas de la conversión de cadenas. Hay casos prácticos en que necesitará hacer llamadas a funciones API, y entonces tendrá que considerar esta compleja situación.

Formateo de cadenas

Usando el operado 'más' (+) y algunas de las funciones de conversión (como IntToStr) se puede construir cadenas complejas a partir de valores existentes. Sin embargo, hay otra forma de enfocar el formateo de números, valores de unidades monetarias, y otras cadenas, para conseguir una cadena final. Puede usar la potente función Format o una de sus funciones anejas.

La función Format requiere como parámetros una cadena con el texto básico y algunos marcadores de formato (normalmente indicados mediante el prefijo %) y un vector de valores, uno por cada marcador. Por ejemplo, para convertir el formato de dos números en el de una cadena, se puede escribir :

Format ('First %d, Second %d', [n1, n2]);

donde n1 y n2 son dos valores enteros (Integer). El primer marcador se reemplaza por el primer valor, el segundo por el segundo, y así sucesivamente. Si el tipo de salida del marcador (indicado por la letra que sigue al %) no encaja con el tipo del parámetro correspondiente, se produce un error de ejecución. No disponer de ninguna comprobación durante la compilación es, de hecho, la mayor desventaja de usar la función Format.

La función Format usa un parámetro de vector abierto (un parámetro que puede tener un número arbitrario de valores), algo que discutiré hacia el final del presente capítulo. Por el momento, sin embargo, tenga en cuenta que sólo sintaxis de tipo vector de la lista de valores que se transmite como segundo parámetro.

Aparte de usar %d, se puede hacer uso de muchos otros marcadores definidos por esta función, y que se enumeran en la Tabla 7.1. Estos marcadores proporcionan una salida por defecto para el tipo de datos dado. De cualquier modo, puede usar especificadores de formatos para alterar la salida por defecto. Un especificador de anchura, por ejemplo, determina un número fijo de caracteres en la salida, mientras un especificador de precisión especifica el número de cifras decimales. Por ejemplo,

Format ('%8d', [n1]);

convierte el número n1 en una cadena de ocho caracteres, alineando el texto a la derecha (use el símbolo 'menos' para especificar justificación a la izquierda), llenándola con espacios en blanco.

Tabla 7.1: Especificadores de tipo, para la función Format.

ESPECIFICADOR DE TIPO

DESCRIPCIÓN

d (decimal) El valor entero correspondiente se convierte en una cadena de cifras decimales.
x (hexadecimal) El valor entero correspondiente se convierte en una cadena de cifras hexadecimales.
p (puntero) El valor de puntero correspondiente se convierte en una cadena de cifras hexadecimales.
s (cadena) La cadena, carácter, o PChar correspondiente se se copia a la cadena de salida.
e (exponencial) El valor de coma flotante correspondiente se convierte en una cadena escrita en notación exponencial.
f (coma flotante) El valor de coma flotante correspondiente se convierte en una cadena escrita en notación de coma flotante.
g (general) El valor de coma flotante correspondiente se convierte en una cadena escrita en notación decimal, lo más corta posible.
n (número) El valor de coma flotante correspondiente se convierte en una cadena escrita en notación de coma flotante con separadores de millar.
m (dinero) El valor correspondiente de coma flotante se convierte en una cadena que representa una cantidad en cierta moneda. La conversión se basa en las opciones regionales -- vea la el apartado "Currency and date/time formatting variables" ayuda de Delphi.

El mejor camino para ver ejemplos de estas conversiones es experimentar con cadenas de formato usted mismo. Para hacer esto más sencillo, he escrito el programa FmtTest, que permite al usuario proporcionar cadenas de formato para números enteros y de coma flotante. Como puede observar en la Figura 7.2, este programa muestra un formulario dividido en dos partes. La parte izquierda es para números enteros (Integer), la derecha para números de coma flotante.

En cada parte hay una primera casilla de edición con el valor numérico que quiere cambiar a formato de cadena. Bajo la primera casilla de edición hay un botón para realizar la operación de formato y mostrar el resultado en una ventana de mensajes. Aparte hay otra casilla de edición, donde puede escribir una cadena de formato. Como alternativa, puede, sencillamente, hacer clic en una de las líneas del componente ListBox, más abajo, para seleccionar una cadena de formateo predefinida. Cada vez que introduzca una nueva cadena de formateo, se añade a la correspondiente casilla de lista (advierta que cerrando el programa perderá estos nuevos ítems).

Figura 7.2: La salida de un valor de coma flotante del programa FmtTest.

El código de este ejemplo simplemente usa el texto de los varios controles para producir la salida. Este es uno de los tres métodos conectados con los botones Show :

procedure TFormFmtTest.BtnIntClick(Sender: TObject);
begin
  ShowMessage (Format (EditFmtInt.Text,
    [StrToInt (EditInt.Text)]));
  // si el elemento no está, añádelo
  if ListBoxInt.Items.IndexOf (EditFmtInt.Text) < 0 then
    ListBoxInt.Items.Add (EditFmtInt.Text);
end;

El código, básicamente, efectúa la operación de formateo, usando el texto de la casilla de edición EditFmtInt y el valor del control EditInt. Si la cadena de formato no está ya en la casilla de lista, se añade a la misma. Si el usuario, en lugar de ello, hace clic en un elemento de la casilla de lista, el código traslada tal valor a la casilla de edición :

procedure TFormFmtTest.ListBoxIntClick(Sender: TObject);
begin
  EditFmtInt.Text := ListBoxInt.Items [
    ListBoxInt.ItemIndex];
end;


Conclusión

Las cadenas de texto son, ciertamente, un tipo de datos muy común. Aunque se pueden usar sin reparos en la mayoría de los casos, sin entender cómo funcionan, este capítulo debe de haber dejado claro cómo se comportan exactamente las cadenas, haciendo posible usar todo el poder de este tipo de datos.0

Las cadenas se manejan en la memoria de una forma dinámica especial, como ocurre con los vectores dinámicos. Este es el tema del capítulo que sigue.


Capítulo siguiente: Memoria

© Copyright Marco Cantù, Wintech Italia Srl 1995-99
© Copyright de la traducción, Rafael Barranco-Droege, 2000