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.
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'.
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.
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.
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.
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.