Anexo 1: Cadenas de caracteres o strings estilo 'C'

Que es una cadena de caracteres en C/C++?

En cualquier lenguaje de programacion las cadenas de caracteres tienen una importancia especial, no solo porque es el tipo mediante el cual se almacenan los mensajes a pantalla o entradas del teclado, sino porque un caracter (char) es del tamaño de un byte, y un byte es la menor unidad de informacion 'natural' para la maquina. Una cadena de caracteres es una coleccion ordenada de bytes. Un fichero (archivo), la informacion de pantalla en el sector de memoria de video, las entradas de bytes por los puertos y muchas otras entidades se pueden conceptualizar comodamente como esto: una coleccion ordenada de bytes. Es cierto que en muchos casos se adoptan tipos definidos o clases, para una mejor administracion de datos, pero aun en estos casos tales estructuras de datos complejas suelen utilizar arrays de tipo char en su nivel mas elemental.

Hay lenguajes que tienen un tipo (type) preestablecido para tratar con cadenas de caracteres, es asi en las distintas versiones de basic, donde el tipo 'String' es un tipo mas (como 'integer'), y asi como existen arrays de enteros hay arrays de strings. En cambio en C/C++ podria decirse que el tipo 'String' no existe como tal  (en C++ hay implementaciones de la clase 'String' pero no pertenecen propiamente al lenguaje sino a librerias anexas, dependiendo de cada compilador), el recurso usado por la mayoria de las funciones de estos lenguajes es representar una cadena de caracteres como un array de elementos tipo char, un array de caracteres.

El solo hecho de que una cadena (string) sea un array plantea dudas a quien viene de otros lenguajes, por ej: como representar entonces un array de strings? (un array bidimensional no es comodo de manejar), cual es el largo permitido de una cadena de caracteres? que sucede si ese largo se modifica? como conocer en tiempo de ejecucion el limite de ese array? etc.

Una cadena de caracteres, representada en memoria, es una simple sucesion de bytes, cada caracter se corresponde con un byte, si queremos sacar en pantalla una cadena de caracteres, el problema es: como sabe el programa donde finaliza esa cadena?, cual es su ultimo byte? A esta pregunta diferentes lenguajes plantean diferentes respuestas, segun un modelo de 'string', los dos modelos clasicos son el de Pascal y el de C.
1-En Pascal el primer byte es reservado para almacenar el largo de cadena, es decir que la cadena de caracteres propiamente dicha comienza en la segunda posicion. Si solo hay 1 byte de espacio para almacenar el largo el maximo permitido para una cadena sera de 255 bytes. Para cadenas de mayor extension habra que utilizar algun otro recurso.
2-En C se reserva una funcion especial al caracter cuyo valor ascii es 0 (cero), ese caracter indicara con su presencia que la cadena finaliza alli, ese char forma parte de la cadena pero por convencion no se lo tiene en cuenta al determinar el largo de la cadena. De una cadena como "hola" se dice que tiene 4 caracteres, aunque en memoria luego de 'a' se encuentre el '\0' que forma parte de ella.
En memoria esa celda que indica el fin de cadena tendra el valor 0, insistimos en que no se trata de un elemento extra (como el EOF de ficheros) sino del char que en el juego de caracteres ascii corresponde al cero.

Ambos modelos de cadena presentan ventajas e inconvenientes. En el modelo 'Pascal' es muy rapida la operacion de encontrar la longitud de cadena (se consulta el primer byte) mientras que en C/C++ hay que recorrer toda la cadena en busqueda del char '\0'. Por otra parte una cadena tipo C no tiene ninguna limitacion en longitud (salvo las indicadas por el modelo de memoria utilizado o razones de hardware) mientras que en Pascal tendra un limite dictado por el byte que almacena el largo.

El rol que juega el caracter '\0' es absolutamente esencial para comprender y manejar fluidamente cadenas de caracteres en C y C++, y la mayor parte de los problemas y bugs tienen relacion con accidentes y descuidos en relacion a ese caracter.
Seria un error creer que en C/C++ el largo de la cadena esta registrado en algun sitio y que luego, en un segundo momento, el programa situa un '\0' en esa posicion, no!, el largo de cadena no existe como dato en ningun sitio. Existe una funcion standard que nos retorna un entero con el largo de una cadena enviada como parametro, es  strlen(char*), lo que hace esta funcion es simplemente contar caracteres hasta que encuentra el '\0', para una masa muy grande de bytes se podria pensar en una funcion menos costosa, pero strlen funciona de ese modo.

Array y cadena de caracteres: algunas diferencias

En muchos textos sobre C y C++ se encuentra la siguiente afirmacion:
  "En C/C++ una cadena de caracteres (string) es un array de caracteres"
es claro en que sentido esta afirmacion es valida, una cadena de caracteres es un array y no una variable de tipo individual, como un entero o un float, sin embargo hay ligeras diferencias entre los dos conceptos, el de array y el de cadena de caracteres..
Mientras que un array de caracteres: es un conjunto ordenado de 'n' bytes de cualquier valor, una cadena de caracteres es solo un subconjunto de ese array, desde el primer char hasta el primer '\0' encontrado en el array. En realidad no es totalmente exacto decir 'subconjunto', pues si por error nuestro array de caracteres no tiene un '\0', las funciones standard lo buscaran fuera del array, y asi la cadena de caracteres llegara a tener mayor extension que el propio array, desbordando su capacidad.
Luego de la declaracion-inicializacion siguiente:

char cad[20] = "hola";

el largo de la cadena de caracteres es 4, valor que se puede obtener con el llamado "strlen(cad)", sin embargo el largo del array es 20, pues siguen siendo 20 los bytes en memoria asociados al nombre 'cad', este valor puede ser obtenido con:

sizeof(cad);

Este valor de limite de array no impide que se pueda sobreescribir mas alla del mismo utilizando un indice que exceda el sizeof() del mismo (generalmente un grave error), para evitarlo se podria consultar ese valor numerico. Lamentablemente es un recurso que no esta disponible cuando se pasan arrays como argumentos a otra funcion, pues todo array es recibido por la funcion llamada a traves de un puntero a su elemento inicial.
El hecho de que sizeof(cad) y strlen(cad) ofrezcan dos valores diferentes justifica el que se hable de diferencias entre los dos conceptos. A este respecto se pueden formular dos preguntas interesantes:

-Es posible que una cadena de caracteres tenga dos bytes con valor 0 ?
-Es posible que un array de caracteres tenga dos bytes con valor 0?

Si uno se atiene a las definicion estricta de cadena de caracteres la respuesta a la primera pregunta es 'NO', en una cadena de caracteres hay un solo byte (char) con valor 0, y coincide con su ultimo elemento. 
En cambio, segun creo, la segunda pregunta debe responderse afirmativamente, un array de caracteres no cambia de tamaño durante la ejecucion de un programa, si lo hemos declarado de 20 char seguira siendo de 20 hasta el final, hay 20 bytes en memoria que le pertenecen solo a ese array. Y nada impide que dos o mas de esos bytes tengan el valor 0.

La cuestion no es solo teorica, supongamos que queremos elaborar un programa para analizar datos de ficheros binarios, por ejemplo de ficheros EXE, en tal caso nos encontraremos con bytes que valen 0 y estan en cualquier posicion, aqui no tiene sentido el pensar en esas colecciones de bytes como 'cadenas de caracteres', no son 'palabras' ni 'texto', sin embargo queremos hacer un programa que lea y almacene los bytes en un array, y construir funciones que analicen sintacticamente ese flujo de bytes.
Algunas funciones standard de lectura de filas detienen cada lectura ante un '\n' o ante un '\0', pero hay otras que permiten leer conjuntos de bytes y los almacenan cualquiera sea su valor, y puesto que son bytes, y estos se correponden con el tipo char, no tenemos otra opcion que almacenarlos en arrays de caracteres. Ahora bien, la mayoria de las funciones de tratamiento de cadenas, como las de string.h, no nos seran utiles, pues interpretan el char 0 como corte de una cadena de caracteres, por lo tanto deberemos construir funciones alternativas.

Teniendo en cuenta las diferencias enumeradas respecto a array y cadena de caracteres, podriamos adoptar la siguiente definicion: -En C y C++, una cadena de caracteres es un array de caracteres terminado en '\0'.

Aseguramiento del fin de cadena

Cuando declaramos un array o una variable cualquiera sin darle un valor inmediatamente, sin 'inicializarla', esa variable o array pueden contener cualquier valor, se dice que su valor es indeterminado, las variables globales son una excepcion pues son inicializadas con un valor default por el compilador, pero la mayor parte de los datos seran locales a una funcion (sea main() o cualquier otra) y por lo tanto no seran inicializados automaticamente.
Supongamos ahora las siguientes lineas de codigo:

int main () 
{
int largo;
char cad[5]; 
largo = strlen(cad);
........................etc.

La pregunta es "cual es el valor de la variable 'largo'?", y el error es creer que ese valor deba ser necesariamente 5, de hecho el valor de 'largo' podria ser 0, 1, 9932, 234, o casualmente 5. Porque razon?, con la declaracion de 'cad' hemos declarado un array de caracteres, reservando 5 bytes de memoria estatica para ese array, pero no hemos inicializado ningun dato, lo mas probable es que esa region de memoria conserve datos aleatorios de algun programa anterior, el valor que nos dara "strlen(cad)" se basara en haber comenzado a contar caracteres en memoria hasta encontrar el '\0', que podria estar en cualquier sitio, si se encontro en el primer byte inspeccionado el valor de 'largo' sera 0.
Si sacamos en pantalla el contenido de esa cadena, sea con printf ("%s", cad) de C, o con cout<<cad de C++, su contenido sera totalmente arbitrario y muy probablemente veamos caracteres 'extraños', esto ocurrira porque las funciones de impresion en pantalla tambien confian en el char 0 para determinar el fin de cadena.
Los dos siguientes graficos muestran un contenido hipotetico de los bytes de memoria asociados a la variable 'cad', son los primeros cinco bytes representados. En el primero se muestra un estado posible luego del codigo anterior, los primeros cinco bytes estan reservados para el array de caracteres 'cad', pero en memoria esos bytes contienen un valor aleatorio e independiente, en este caso si se llamara a la funcion strlen(cad) retornaria el entero 15 (cuenta 15 antes de encontrar el 00), y si se sacara en pantalla con "printf" o "cout" se verian los caracteres de la izquierda (subrayados con rojo).



En cambio, si al declarar el array lo inicializamos,

char cad[5]="hola";

o bien en un primer paso lo declaramos y luego copiamos "hola" en la cadena con strcpy()

char cad[5];
strcpy(cad, "hola");

en ambos casos estara asegurada la presencia del '\0' que indica el fin de cadena.

Ahora strlen() nos indicaria que el largo de cadena es 4, y las funciones de impresion en pantalla funcionaran normalmente, todo gracias a la presencia del char '\0'.

La idea es muy simple, sin embargo es necesario cometer muchos errores y desarrollar mucha practica con cadenas 'tipo C' antes de sentirse comodo con ellas. Las librerias "string.h" y "mem.h" contienen muchas rutinas para tratar con cadenas de caracteres, es necesario conocer en detalle el modo en que cada funcion trata la cuestion del '\0' final para no encontrarse con sorpresas.

Ejemplos de funciones standard

Como ejemplo observaremos el modo de operar de "strset" y "memset" y las precauciones necesarias en relacion al valor '\0'.

Memset (char *cad, int ch, int n);

Esta funcion setea los primeros 'n' char de una cadena al valor pasado como segundo argumento (ch). Si queremos setear los primeros 5 bytes de una cadena con espacios se podria llamar a la funcion del siguiente modo:

     memset (cad, ' ', 5);

La operacion es muy simple y aparentemente inofensiva, sin embargo !nada relacionado con cadenas en C/C++ es simple!. Esta funcion no hace nada en relacion al '\0', ni lo usa ni lo setea, podria ocurrir que antes de la anterior linea de codigo el array 'cad' tuviera la cadena "uno", con el cero en su cuarto byte, en tal caso el llamado a memset estaria sobreescribiendo ese cero y el limite pasaria a ser indeterminado. Si nuestro proposito es que la cadena contenga solo esos cinco bytes seteados es muy importante que, luego de haber invocado a memset, nos aseguremos del fin de cadena, por ejemplo con:

   cad[5]='\0';        //Tambien es posible  "cad[5]=0", pues el 0 (entero) 
                       //se convierte implicitamente en 0 (char)

Podria ser el caso de que tuvieramos muy claro que el fin de cadena esta mas alla de esos 5 bytes y lo quisieramos conservar, pero si solo nos interesa una cadena con el caracter ascii 'ch' repetido 'n' veces, una buena practica sera asegurarnos el fin de cadena con "cad[n]=0", para evitar problemas posteriores.

Strset (char *cad, int ch);
La explicacion de ayuda en linea de Borland dice que strset "setea todos los caracteres de una cadena a 'ch' ". Esto significa que si queremos que 'toda' nuestra cadena 's' pase a estar compuesta por asteriscos (ascii=42) podemos escribir:

    strset (cad, '*');      // es equivalente "strset (cad, 42)"

La diferencia con memset() es bastante clara, aquella era para setear 'n' caracteres y esta para 'toda' la cadena, y ahi esta el principal peligro, en la palabra 'toda'. Pues esta fucion confia exclusivamente en el '\0' para determinar el fin de la cadena que seteara. Supongamos un programa que comenzara con las siguientes lineas:

    int main () 
     {
     char cad[20];
     strset (cad, '*');
     .....................etc.

Estas lineas de codigo son muy peligrosas, todo depende de si la funcion encuentra o no un '\0' antes de llenar de asteriscos todo lo que encuentre, hasta llegar al fin del segmento, donde sobreescribiria los valores de stack para salir bien de main() y posiblemente habria que resetear la maquina.

La explicacion de la funcion dada por la ayuda de TurboC++ es demasiado escueta y no menciona este tipo de problemas, la enunciacion es correcta: "setea toda la cadena" con el caracter indicado, pero es facil olvidar que significa esto en los lenguajes C/C++, significa "todo lo que encuentre hasta dar con un '\0' "

Podria darse una lista de posibles errores para los usos de cada una de las funciones que involucran a cadenas. Todos estan relacionados con el problema del limite del array de caracteres. Se mencionan a continuacion algunos de los muchos errores posibles.

Sobreescritura de variables

Ya se vio un ejemplo con strset() de como es posible exceder los bytes que corresponden a un array de caracteres y sobreescribir bytes que no le corresponden a esa variable. Veamos un ejemplo simple que involucra a la funcion strcat.
La funcion strcat(cad1, cad2) concatena dos arrays de caracteres 'pegando' el segundo argumento luego del primero.

int main () {
char cad1[] = "hola";
char cad2[] = " mundo";
strcat (cad1,cad2);
....................etc

Lamentablemente estas lineas seran problematicas, el modo en que se declararon los arrays hace que para 'cad1' haya cinco bytes en memoria ("hola" mas el '\0') y 6 bytes para 'cad2'. El resultado esperable seria el de que 'cad1' tuviera ahora la cadena "hola mundo", pero esto implica 10 bytes mas el '\0', lo cual desborda la capacidad de cad1. La funcion strcat() hara lo que se le pide de todos modos, sobreescribiendo de ese modo bytes que pertenecen a otras variables.
Muchas veces el problema no aparece inmediatamente sino cuando se intenta operar con las variables 'pisadas', en todo caso hay que tener en claro que es necesario reservar los suficientes bytes de memoria al usar una variable, ya se trate de una reserva estatica o dinamica.

Distintos sintomas de sobreescritura de variables

Los efectos de sobreescribir una variable son muy variados, dependiendo en parte del tipo de dato sobreescrito y de la operacion involucrada. Algunos ejemplos sacados de la practica:
1-El corte imprevisto, durante el transcurso de un programa, como un flujo de archivo puede ser provocado por la sobreescritura del dato 'fstream' o el puntero "FILE*".
2-La aparicion de caracteres ascii 'extraños' casi siempre se debe a la supresion de un '\0' de fin de cadena. Si se escucha un pitido (beep) esto solo significa que entre los caracteres 'extraños' estaba el ascii =7.
3-Sobreescribir el comienzo del segmento de datos provoca el mensaje "Null pointer assignment", este tema se tratara con mas detalle en el apartado dedicado a problemas tipicos con punteros.

Son solo algunos ejemplos, los sintomas de sobreescritura de variables son tan variados como las multiples posibilidades que produciria dar un valor random, de modo no controlado, a uno o mas datos de nuestro programa.

PRINCIPAL




















Free Web Hosting