Los rincones del MFC: La clase CString

Cadena
En este artículo vamos a profundizar en una de las clases más importantes dentro del framework MFC. La clase CString nos permite olvidarnos de uno de los mayores problemas a la hora de programar en C/C++, es decir: el tratamiento de cadenas de texto.

Lo básico

Un objeto CString representa una secuencia de variables de tipo TCHAR, es decir caracteres alfanuméricos, de 1 ó 2 bytes cada uno de ellos, dependiendo de si el símbolo _UNICODE está definido o no. Esta secuencia, además es de longitud variable, pudiendo añadir, eliminar, concatenar nuevos caracteres y cadenas.

La clase se define en el archivo de cabecera "afx.h", y se implementa en 4 archivos: "strcore.cpp", "winstr.cpp", "strex.cpp" y "afx.inl".

Para utilizar esta clase debemos hacer un include de "stdafx.h", y no de "afx.h" como podría parecer lógico. Esto es debido a que al incluir "stdafx.h", estamos incluyendo el paquete de MFC personalizado para nuestro proyecto, y no el MFC completo.

De todas formas, los asistentes de Visual C++ hacen este paso por nosotros, creando un archivo "stdafx.h", según el uso del MFC que vamos a hacer, e incluyéndolo en los archivos de nuestro proyecto.

Esta clase, no hereda de CObject como la gran mayoría de las clases de MFC, por lo que no puede beneficiarse de la serialización o información en tiempo de ejecución (a través de la estructura CRuntimeClass).

Esto es así dado que la clase CString es de uso intensivo dentro de cualquier aplicación MFC, por lo que en su diseño se dio mucha importancia a su ocupación en memoria y velocidad de ejecución.

La clase define atributos, funciones miembro y operadores que permiten simplificar mucho el tratamiento de cadenas de caracteres, y olvidarnos de las típicas funciones de C pensadas para los arrays de chars: strcmp, strcpy, strlen, etc.

Como cualquier otra clase, podemos instanciar variables a través de objetos, punteros o referencias, como en los siguientes ejemplos:

Objeto

La memoria de los objetos se asigna en la pila, como cualquier otra variable automática o local.

No es necesario reservar ni liberar explícitamente la memoria, ya que las variables automáticas lo hacen de forma implícita.

La creación de estas variables es muy rápida, ya que el espacio de memoria de la pila ya está previamente reservado, aunque hay que tener cuidado de no crear demasiadas variables de este tipo, ya que podríamos desbordar la pila.

Para más información sobre este tipo de variables se puede consultar el artículo sobre {ar:pila:la pila en Win32}.

{
    // se declara el objeto y se reserva
    // la memoria automaticamente
    CString    objeto = "hola";

    // se utiliza el operador "="
    objeto = objeto + " mundo";

    // la memoria se libera de forma
    // automática
    return;
}

Puntero

La memoria de los punteros se reserva en el montón, permitiendo así dejar más espacio libre en la pila. Además, de este modo, podemos controlar la reserva/liberación del objeto, ya que la reserva dinámica de memoria requiere de operaciones explícitas para la creación/destrucción de memoria.

Hay que prestar atención a la velocidad de ejecución, ya que puede ser bastante más lenta que con variables automáticas, sobre todo si la función va a ser ejecutada múltiples veces.

Para más información sobre estas variables se puede consultar el artículo sobre {ar:montones:los montones en Win32}.

{
    CString    *puntero;

    // se reserva la memoria en el montón
    puntero = new CString("hola");

    // se asigna con el operador "="
    *puntero = *puntero + " mundo";

    // se libera la memoria del montón
    delete puntero;

    return;
}

Referencia

Las variables referencia son muy parecidas a los punteros, con la única excepción de que no es posible realizar aritmética de punteros con ellas (desplazamiento del puntero por la memoria, desreferencia, etc.).

Estas variables no implican una asignación dinámica de memoria, ya que pueden "apuntar" a cualquier zona de memoria, ya sea una variable creada en la pila (automática) o en el montón (dinámica).

Normalmente, se utilizan en parámetros de funciones, para pasar objetos por referencia, pudiendo evitar su modificación.

Para más información sobre las variables automáticas y dinámicas puedes consultar los {ar:pila:la pila} y {ar:montones:los montones} en Win32.

void SetHolaMundo(CString &referencia)
{
    // el parámetro "referencia" en realidad
    // es un puntero a un objeto CString,
    // pero se usa como si no fuera puntero.

    // se asigna con el operador "="
    referencia = referencia + " mundo";

    // al ser referencia, se utiliza el
    // operador "." en vez del operador de
    // indirección "->" propio de los
    // punteros
    referencia.MakeLower();

    return;
}

Estructura interna

La clase CString cuenta solamente con un atributo, el cual que almacena el buffer donde residirán finalmente los caracteres. Las funciones miembro que utilizamos, lo único que hacen es interpretar y manipular este buffer, facilitando así la vida al programador.

Este atributo de define como

protected:
   LPTSTR m_pchData;

El buffer al que apunta, se encuentra dentro de una estructura para uso privado de la clase CString: la estructura CStringData, que en ciertas ocasiones se comparte entre varios objetos CString. Esta estructura se define como:

     struct CStringData
     {
        long nRefs;             // nº de objetos CString que comparten esta estructura
        int  nDataLength;       // longitud de la cadena (contando con el '\0')
        int  nAllocLength;      // longitud del buffer reservado

        TCHAR* data()           // retorna un puntero a la cadena
        {
            return (TCHAR*) (this+1);
        }
    };

El cometido de esta estructura es doble: por un lado realizar la gestión de referencias, almacenando un contador (nRefs) que gestiona el número de objetos CString que utilizan la misma estructura CStringData.

Por otro lado, gestionar una caché interna para almacenar los caracteres.

De este modo, cuando creamos un CString para almacenar 10 caracteres, en realidad el buffer interno ha reservado espacio para 64, aunque sólo utilice 10. Si después concatenamos 20 nuevos caracteres, no tenemos que reservar un nuevo bloque de memoria, ya que el buffer inicial cuenta con espacio suficiente para la nueva cadena (de 30 caracteres). En caso de sobrepasarse el límite del buffer, se direcciona un nuevo espacio de un tamaño superior al necesitado, aplicando un crecimiento exponencial (64, 128, 256, 512, 1024...)

Tanto el contador de referencias, como el buffer interno, se acceden continuamente desde distintos métodos de la clase, aunque no vamos a profundizar demasiado en este aspecto.

Constructores

El punto inicial de cualquier objeto es el constructor, así que es necesario conocer los distintos tipos que se nos ofrece para escoger el más adecuado.

Los constructores definidos son los siguientes:

CString();
Crea e inicializa un nuevo objeto vacío.
CString(IN const CString& origen);
Crea un nuevo objeto a partir de otro CString, y hace que la estructura CStringData quede compartida entre ambos objetos, incrementando el contador de referencias para marcar que la misma estructura CStringData está siendo compartida.
Si después se realiza alguna modificación sobre el contenido, se creará una nueva estructura CStringData para el nuevo valor (a través de la función protegida CopyBeforeWrite), dejando así de compartirse los datos, y administrando cada objeto su propia estructura interna. Para comprobar esto puedes ver la aplicación de ejemplo en la que se muestra el valor y dirección de la estructura CStringData en varias situaciones.
CString(IN TCHAR ch, IN DEFAULT int nRepeat = 1);
Crea un nuevo objeto e inicializa el buffer interno a una cadena con el carácter "ch" repetido "nRepear" veces.
CString(IN LPCSTR lpsz);
CString(IN LPCWSTR lpsz);
Crea un nuevo objeto e inicializa el buffer interno a la cadena pasada en el puntero "lpsz".
Existe una versión para cadenas ANSI (1 byte por carácter) y otra para cadenas UNICODE (2 bytes por carácter).
CString(IN LPCSTR lpch, IN int nLength);
CString(IN LPCWSTR lpch, IN int nLength);
Crea un nuevo objeto e inicializa el buffer interno a los "nLength" primeros caracteres de la cadena pasada en el puntero "lpsz".
Existe una versión para cadenas ANSI (1 byte por carácter) y otra para cadenas UNICODE (2 bytes por carácter).
CString(IN const unsigned char psz);
Crea un nuevo objeto e inicializa el buffer interno al contenido del array de chars "psz".

Hay que tener en cuenta, que todos los constructores suponen una reserva inicial de memoria, por lo que son susceptibles de lanzar la excepción CMemoryException.

Funciones miembro

Aunque es recomendable saber qué está pasando cada vez que utilizamos un objeto CString, el manejo general se realiza a través de su funciones miembro y opcionalmente los operadores sobrecargados.

Algunas de las funciones miembro más importantes son:

int GetLength() const;
Retorna el número de bytes que ocupa la cadena, sin tener en cuenta el terminador '\0'. Concretamente, lo único que retona es el valor del campo "nDataLength" de la estructura CStringData asociada.
Hay que tener en cuenta que en compilaciones UNICODE, cada carácter ocupa 2 bytes, por lo que si almacenamos una cadena de 4 caracteres, GetLength() retornará 8 bytes.
Es una función "const", es decir: informativa, que no modifica el estado del objeto.
BOOL IsEmpty() const;
Retorna true si la cadena está vacía, es decir si el número de bytes que retorna GetLength() es 0.
void Empty();
Vacía el contenido del objeto, liberando la estructura interna CStringData si no está compartida con otros objetos.
TCHAR GetAt(IN int nIndex) const;
Retorna el valor del carácter número "nIndex", empezando desde el índice 0.
La llamada a esta función es equivalente al uso de operador [].
nternamente, GetAt(x) no hace más que retornar la posición x del buffer interno, es decir: m_pchData[x].
void SetAt(IN int nIndex, IN TCHAR ch);
Establece el valor del carácter "nIndex", empezando a contar desde el índice 0.
La llamada a esta función es equivalente a asignar un valor a través del uso del operador [].
Si "nIndex" es mayor que el número de caracteres existentes, la cadena no crecerá para almacenar dicho valor.
int Compare(IN LPCTSTR lpsz) const;
Retorna 0 si el valor de la cadena "lpsz" es igual al valor almacenado por el objeto, un valor negativo si el objeto es menor que "lpsz" y un valor positivo si el objeto es mayor que "lpsz".
Esta función tiene en cuenta las mayúsculas y minúsculas, aunque se puede utilizar la función CompareNoCase() para ignorar las mayúsculas/minúsculas en la comparación.
int Collate(IN LPCTSTR lpsz) const;
Retorna 0 si el valor de la cadena "lpsz" es igual al valor almacenado por el objeto, un valor negativo si el objeto es menor que "lpsz" y un valor positivo si el objeto es mayor que "lpsz".
Esta función tiene en cuenta las mayúsculas y minúsculas, aunque se puede utilizar la función CollateNoCase() para ignorar las mayúsculas/minúsculas en la comparación.
Para decidir si una cadena es menor que otra, tiene en cuenta el conjunto de caracteres locales del equipo.
CString Mid(IN int nFirst, IN OPTIONAL int nCount) const;
Retorna un objeto CString con la cadena resultante de extraer, empezando en 0, desde la posición "nFirst" hasta una longitud de "nCount" caracteres.
Si no se indica el parámetro "nCount", se retornará hasta el final de la cadena.
También se pueden utilizar las funciones Right(nCount) y Left(nCount) para extraer "nCount" caracteres desde la derecha o izquierda respectivamente.
CString SpanExcluding(IN LPCTSTR lpszCharSet) const;
Retorna un objeto CString que es la subcadena del objeto "this".
Esta subcadena comienza en el primer carácter no existente en el conjunto de caracteres representado por "lpszCharSet", y continúa hasta el primero que coincida con algudo de "lpszCharSet".
Esta función es muy útil para trocear cadenas hasta cierto delimitador, como lo haría un parser.
Por ejemplo:
  CString cadena = "trozo1;trozo2,trozo3,trozo4";
  // se muestra "trozo1", ya que trocea
  // hasta el primer carácter
  // que encuentra en el conjunto.
  AfxMessageBox( cadena.SpanExcluding(";,"), 0, 0 );
También existe la función contraria: SpanIncluding, que retorna la cadena desde el primer carácter que se encuentre en el subconunto, hasta el siguiente que no aparezca:
  CString cadena = "hola mundo";
  // se muestra la cadena "hola mu",
  // porque el conjunto de caracteres contiene
  // las vocales y las consonantes "h", "l" y "m".
  AfxMessageBox( cadena.SpanIncluding("aeiouhlm"), 0, 0 );
void MakeUpper();
void MakeLower();
Convierte todos los caracteres de la cadena a mayúsculas o minúsculas.
void FreeExtra();
Libera el espacio sobrante dentro del caché interno del objeto CString. Esto provoca que el tamaño del buffer interno se ajuste al tamaño de la cadena contenida, por lo que si se amplía el contenido, será necesario reservar un nuevo buffer.
int Remove(IN TCHAR chRemove);
Elimina todas las ocurrencias del carácter "chRemove" del contenido del objeto. Retorna el número de caracteres eliminados.
int Delete(IN int nIndex, IN DEFAULT int nCount = 1);
Elimina "nCount" caracteres desde la posición "nIndex" (empezando en 0).
int Insert(IN int nIndex, IN TCHAR ch);
int Insert(IN int nIndex, IN LPCTSTR pstr);
Inserta la cadena o carácter pasado por parámetro en la posición "nIndex" (a partir de 0) del contenido de la cadena. Si el índice es mayor que la longitud de la cadena, se insertará al final, es decir: se concatenará.
void AFX_CDECL Format(IN LPCTSTR lpszFormat, ...);
void AFX_CDECL FormatMessage(IN LPCTSTR lpszFormat, ...);
void FormatV(IN LPCTSTR lpszFormat, IN va_list argList);
Modifica el contenido del objeto aplicando un formato, del mismo modo que con las funciones de C: sprintf(), wsprintf() y wvsprintf().
FormatMessage() utiliza la función del API Win32 FormatMessage(), mientras que Format() llama a FormatV(), la cual acaba utilizando vsprintf() para realizar la convesión.
int Replace(IN TCHAR chOld, IN TCHAR chNew);
int Replace(IN LPCTSTR lpszOld, IN LPCTSTR lpszNew);
Reemplaza todas las ocurrencias de "chOld" o "lpszOld" por "chNew" o "lpszNew".
void TrimLeft();
void TrimRight();
Elimina todos los espacios a la izquierda/derecha de la cadena.
int Find(IN TCHAR ch, IN OPTIONAL int nStart);
int Find(IN LPCTSTR lpszSub, IN OPTIONAL int nStart);
Busca el carácter "ch" o la subcadena "lpszSub" a partir de la posición "nStart" (comenzando en 0). Si no se indica "nStart", comenzará desde el principio de la cadena.
También existe la función ReverseFind() que busca un carácter dado comenzando desde el final de la cadena, y FindOneOf() que busca cualquier carácter coindicente con un subconjunto dado.
LPTSTR GetBuffer(IN int nMinBufLength);
int ReleaseBuffer(IN DEFAULT int nNewLength = -1);
Retorna un puntero a la cadena almacenada internamiente por el objeto CString. Después de utilizar la cadena, es necesario liberar el buffer a través de ReleaseBuffer.
Más abajo se da información sobre cómo tratar objetos CString como punteros a char de al estilo "C".
LPTSTR GetBuffer(IN int nMinBufLength);
int ReleaseBuffer(IN DEFAULT int nNewLength = -1);
Retorna un puntero a la cadena almacenada internamiente por el objeto CString. Después de utilizar la cadena, es necesario liberar el buffer a través de ReleaseBuffer.
Más abajo se da información sobre cómo tratar objetos CString como punteros a char de al estilo "C".
LPTSTR LockBuffer();
void UnlockBuffer();
Desactiva el contador de referencias, y crea una copia de la cadena que retorna como puntero.
Cuando se termine de manipular la copia, es necesario restablecer el contador de referencias, a través de UnlockBuffer().

Operadores sobrecargados

Para facilitar la programación y hacerla más natural al programador, se introducen ciertos operadores sobrecargados, que normalmente tienen una función equivalente.

Algunos de los más importantes son:

=
Asigna el valor de la izquierda (lvalue) al contenido del objeto CString.
+
Concatena el valor de dos objetos CString, o cualquier combinación entre CString, LPTCSTR y TCHAR.
+=
Concatena el valor de la izquierda (CString, LPCTSTR o TCHAR) al contenido del objeto CString.
==, <, <=, >, >=, !=
Compara el valor de la izquierda con el contenido del objeto CString. Es equivalente a comparar
[]
Accede a cada uno de los caracteres del contenido del objeto CString, comenzando por el índice 0. Es equivalente a la función miembro GetAt(x)
<<, >>
Implementa los operadores de serialización propios del objeto CObject, leyendo o grabando los datos del CString en un objeto de tipo CArchive.
LPCTSTR
Retorna un puntero al buffer interno de caracteres. No será posible modificar este puntero, ya que es un puntero constante.

Uso del CString como LPCSTR

Los objetos CString también pueden utilizarse como si fueran arrays de chars, concretamente del tipo LPCTSTR (puntero largo constante a cadena).

Como ya hemos visto, el operador LPCTSTR permite la conversión de un objeto CString a su puntero LPCTSTR, y cómo uno de los constructores nos permite crear un objeto CString a partir de una cadena LPCTSTR.

Además, existe un nuevo método, para acceder a al array de caracteres de un objeto CString: a través de las funciones GetBuffer() y ReleaseBuffer().

GetBuffer() nos retorna un puntero LPTSTR (puntero modificable) a la cadena interna del objeto CString. Esto nos permite consultar el contenido interno del objeto o incluso modificarlo, aunque en este caso habría que llamar a ReleaseBuffer() después de la modificación.

ReleaseBuffer() nos libera el buffer previamente reservado con GetBuffer(). Siempre hay que llamar a ReleaseBuffer() cuando hayamos terminado de manipular la cadena obtenida con GetBuffer(), y con más razón cuando esta cadena ha sido modificada.

Hasta aquí la parte fácil del asunto, pero... ¿qué pasaría si pasamos un objeto CString a la función fprintf?

Lo primero que se nos pasa por la cabeza es que eso no debe permitirlo el compilador... pero no es así, ya que las funciones con argumentos variables (como fprintf, sprintf, scanf, etc.) no hacen comprobación de tipos.

Entonces... ¿qué es lo que ocurre con el siguiente código?

  CString objeto = "hola mundo";
  char buffer[16];

  ::sprintf(buffer, "%s", objeto);

Cuando la función procese el primer algumento variable (llamado "objeto"), considerará que es un puntero a una cadena de caracteres terminada en '\0'. Y así es, ya que, como hemos visto, la implementación de la clase CString solamente tiene un atributo, y este es un puntero a una cadena de caracteres. Así que la memoria ocupada por el objeto comenzará con un puntero al contenido del propio objeto, seguido por el resto de los componentes de la clase (en nuestro caso, solo funciones miembro).

El código anterior podría ser equivalente a este otro, saltándonos la seguridad del atributo protected:

  CString objeto = "hola mundo";
  char buffer[16];

  ::sprintf(buffer, "%s", objeto.m_pchData);

Este engaño que hace la clase CString a la función sprintf, lo podremos hacer nosotros mismos, siempre y cuando definamos una clase cuyo primer atributo sea un puntero a cadena de texto, y pasemos el objeto, y no la referencia al objeto.

Como norma general, podríamos decir que cuando pasamos un objeto por parámetro, en realidad estamos pasando la dirección de memoria donde está el primer atributo de ese objeto.

Clases auxiliares basadas en CString

Dentro del MFC se definen ciertas clases que se basan en los objetos de tipo CString. Dos de ellas son CStringArray y CStringList.

CStringArray: Almacena un array de objetos de tipo CString, del mismo modo que la clase CObArray. Es posible almacenar cualquier número de punteros a objetos objetos, recuperarlos, modificarlos, etc. Para más información puedes consultar la documentación oficial de Microsoft.

CStringList: Almacena una lista de objetos de tipo CString, del mismo modo que la clase CObList. La diferencia principa con CStringArray es que esta almacena punteros a CString, mientras que CStringList almacena directamente los objetos. Es posible almacenar cualquier número de objetos, recuperarlos, modificarlos, etc. Para más información puedes consultar la documentación oficial de Microsoft.


Creative Commons Creative Commons License 2003 by JM