PUNTEROS DE CARACTERES Y CADENAS








Punteros de caracteres:Definición, Declaración e Iniciación.
Un puntero de carácter es un objeto del tipo siguiente:
char * p;
Los punteros de carácter se utilizan normalmente para acceder a cadenas de caracteres, puesto que éstas están construídas tomando como base una dirección (la del primer carácter) y un marcador de fin de cadena (el carácter '\0'). La declaración de un puntero de caracter sigue las reglas habituales:

char * puntero_caracter;

En este momento puntero_caracter no tiene un valor utilizable. Será precisor dárselo; esto podría hacerse en la forma siguiente:

char letra;
char * puntero;
puntero = &letra;

y se tendrá un alias.

Punteros de cadenas: Definición, Declaración e Iniciación.
La declaración de una cadena puede ser de la forma siguiente:

char cadena[LONGITUD];

Así se reserva espacio para una cadena de LONGITUD caracteres. En C, el nombre de las listas monodimensionales equivale a la dirección del primer elemento de las mismas. Una vez que se dispone de espacio reservado para la cadena, se puede construir un puntero de la cadena empleando las sentencias siguientes:

char * p;
char cadena[LONGITUD];
p = cadena;/* Esto equivale, desde luego, a poner: */
p = &cadena[0];

Esto dará lugar a que en p se almacene la dirección de cadena ; no es un error de asignación, por cuanto no se intenta almacenar una cadena en un carácter, sino que se asigna el valor de la dirección de un carácer a una variable de tipo puntero de char . Esto no pasaría de ser un alias de la cadena... pero suceden cosas interesantes, como puede verse a continuación.

Recorrido de una cadena mediante aritmética de punteros y mediante índices.
El mecanismo de la aritmética de punteros en C tiene su expresión más sencilla en el caso de utilizar, precisamente, punteros de caracteres. Consideremos las declaraciones siguientes:

Tipo_base lista[NUMERO_DE_ELEMENTOS];
Tipo_base *p, *q;
p = lista;

y veamos a continuación la forma en que funciona la aritmética de punteros. Se admite la suma de números enteros (positivos o negativos) a un puntero. esto es, se admiten expresiones de la forma
q = p + 1;

El resultado de estas operaciones es otro puntero de igual tipo. La dirección almacenada en el puntero nuevo es, como siempre, un número. Podría pensarse que al sumar 1 a un puntero se consigue otro puntero tal que el valor numérico del mismo es igual al valor numérico de la primera dirección incrementado en una unidad. Y de hecho, así sucede cuando se trata de un puntero de char, o de algún tipo base cuya extensión sea de un byte.

En general, cuando se suma 1 a un puntero, el valor numérico de la dirección no se incrementa en 1, sino en sizeof(Tipo_base) . Entonces, al sumar i a un puntero se tiene que la dirección obtenida al calcular p+i tiene el valor direccion_de_p + i*sizeof(Tipo_base) . Se ha obtenido un nuevo puntero, que señala una posición de memoria que correspondería a un elemento de una lista situado i posiciones más alla del elemento señalado por p .

Ejemplo .- Comprobar que si p es un puntero de char entonces al sumar 1 a p se obtiene una dirección cuyo valor aumenta en 1 ( = sizeof(char) ) respecto a la dirección de p . En general, comprobar que al sumar i a p (al calcular p+i ) se incrementa el valor de p en i*sizeof(char) .

Solución

#include<stdio.h>

int main(int argc, char * argv[])
{
  char letra;
  char * p = &letra, *q;
  int i;
  unsigned long base;
  printf("La dirección de letra es           %u\n", (unsigned int)p);
  q = p + 1;
  printf("La dirección dada por q = p + 1 es %u\n", (unsigned int)q);
  q = p + 2;
  printf("La dirección dada por q = p + 2 es %u\n", (unsigned int)q);
  q = p + 3;
  printf("La dirección dada por q = p + 3 es %u\n", (unsigned int)q);
  q = p + 4;
  printf("La dirección dada por q = p + 4 es %u\n", (unsigned int)q);
  printf("Escriba el valor de i: ");
  scanf("%d", &i);
  q = p + i;
  printf("q = p + %d vale                     %u\n", i, (unsigned int)q);
  base = (unsigned long)p;
  printf("El valor calculado es: %u\n",base + i*sizeof(char));
  return 0;
}
/*
La dirección de letra es           3221224104
La dirección dada por q = p + 1 es 3221224105
La dirección dada por q = p + 2 es 3221224106
La dirección dada por q = p + 3 es 3221224107
La dirección dada por q = p + 4 es 3221224108
Escriba el valor de i: 5
q = p + 5 vale                     3221224108
*/


Comentarios .- Si se estudian los valores obtenidos en la columna de la derecha, que son los valores de la dirección de doble almacenada en p , y luego los valores de las direcciones obtenidas al calcular p+1 , p+2 , etc., se observa que difieren 1 entre sí. Esto es precisamente lo que se quería poner de manifiesto: si se suma 1 al puntero, se obtiene una dirección situada 1 bytes más allá, porque el tamaño de la variable señalada ( char ) es 1. En otras palabras, el puntero p se comporta como si señalara el primer elemento de una lista de char .

Ejemplo .- Comprobar que si p es un puntero de double entonces al sumar 1 a p se obtiene una dirección cuyo valor aumenta en 8 ( = sizeof(double) ) respecto a la dirección de p . En general, comprobar que al sumar i a p (al calcular p+i ) se incrementa el valor de p en i*sizeof(double) .

Solución

#include<stdio.h>

int main(int argc, double * argv[])
{
  double doble;
  double * p = &doble, *q;
  int i;
  unsigned long base;
  printf("La dirección de doble es           %u\n", (unsigned int)p);
  q = p + 1;
  printf("La dirección dada por q = p + 1 es %u\n", (unsigned int)q);
  q = p + 2;
  printf("La dirección dada por q = p + 2 es %u\n", (unsigned int)q);
  q = p + 3;
  printf("La dirección dada por q = p + 3 es %u\n", (unsigned int)q);
  q = p + 4;
  printf("La dirección dada por q = p + 4 es %u\n", (unsigned int)q);
  printf("Escriba el valor de i: ");
  scanf("%d", &i);
  q = p + i;
  printf("q = p + %d vale                     %u\n", i, (unsigned int)q);
  base = (unsigned long)p;
  printf("El valor calculado es: %u\n",base + i*sizeof(double));
  return 0;
}
/*
La dirección de doble es           3221224104
La dirección dada por q = p + 1 es 3221224112
La dirección dada por q = p + 2 es 3221224120
La dirección dada por q = p + 3 es 3221224128
La dirección dada por q = p + 4 es 3221224136
Escriba el valor de i: 5
q = p + 5 vale                     3221224136
*/


Comentario
Si se estudian los valores obtenidos en la columna de la derecha, que son los valores de la dirección de doble almacenada en p , y luego los valores de las direcciones obtenidas al calcular p+1 , p+2 , etc., se observa que difieren 8 entre sí. Esto es precisamente lo que se quería poner de manifiesto: si se suma 1 al puntero, se obtiene una dirección situada 8 bytes más allá, y no 1. En otras palabras, el puntero p se comporta como si señalara el primer elemento de una lista de números double . Al incrementar el puntero, éste hace las veces de índice de la lista. No acaba aquí la cosa. Considérense las declaraciones siguientes

Tipo_base lista[NUMERO_DE_ELEMENTOS];
Tipo_base *p, *q;
p = lista;

Acabamos de observar que p+i es la dirección de un elemento situado i posiciones más allá de p, donde el tamaño de las posiciones es, a efectos prácticos, el tamaño del Tipo_base medido en bytes. Entonces, a la vista de las declaraciones anteriores, se tiene que ocurre lo siguiente:

p tiene el valor &lista[0];
p + 1 tiene el valor &lista[1];
p + 2 tiene el valor &lista[2];

y, en general

p + i tiene el valor &lista[i];

Dicho con palabras, dado un puntero de Tipo_base cuyo valor es la dirección del primer elemento de una lista, al sumar i al puntero se obtiene la dirección del elemento i-ésimo de la lista. Ahora, bien, según se recordará, el asterisco puede hacer las veces de operador de indirección, esto es, se puede anteponer un asterisco a un puntero, y el resultado es la variable señalada. Entonces, aplicando el operador asterisco, se obtiene lo siguiente:

*p tiene el valor lista[0];
*(p + 1) tiene el valor lista[1];
*(p + 2) tiene el valor lista[2];

y, en general

*(p + i) tiene el valor lista[i];

en donde Lo importante es recordar lo siguiente:

*(p+i) equivale a lista[i]

. Esto significa que diponer de una lista y disponer de un puntero de su primer elemento es completamente equivalente, porque nos permite acceder indistintamente a los elementos de la lista empleando el nombre de ésta o el puntero.
Ha llegado el momento de introducir, como quizá se sospechara, un nuevo operador. El operador [] es otra forma de aplicar un índice y deshacer una indirección, esto es, se tiene la siguiente equivalencia:

p[i] equivale a *(p+i)

Entonces podemos escribir lo siguiente, para cualquier valor de i:

p[i] equivale a lista[i]



El resultado final es que si se tiene un puntero de una lista entonces es posible acceder a los elementos de la lista empleando el puntero con un índice, y esto equivale a usar la aritmética de punteros mencionada.


Véase el oportuno Ejercicio .


Nota .- Acabamos de decir lo siguiente: si se tiene un puntero,
int * p;
entonces se puede aplicar al puntero el operador corchetes con un índice, y efectuar operaciones con p como si se tratase de una lista, en la forma

p[i] = 33;

Hasta aquí, todo bien. Pero hay un problema: nada garantiza que un puntero (sea cual fuere su tipo) señala un bloque de memoria correspondiente a una lista. Podría muy bien ser, en este caso, que se tuviera

int q;
p = &q;


Y entonces la expresión p[i] , que sintácticamente es correcta y admisible totalmente por el compilador, carecería por completo de sentido, sin ser detectada. La expresión p[i] denotaría un inexistente entero situado i posiciones más allá de p . Esto es una catástrofe en potencia y renovamos nuestra indicación, hecha anteriormente: siempre que se declara un puntero, conviene asignarle el valor NULL . De este modo, todo intento de acceder a la(s) variable(s) que señala se detectará de inmediato.

Funciones de cadenas ( string.h ).
El archivo de encabezado string.h contiene las descripciones de toda una gama de funciones adecuadas para la manipulación de cadenas. Véase a continuación una breve descripción de estas funciones (se hallará un listado menos extenso aquí .


void bcopy(const void *src, void *destino, size_t num) Sirve para copiar num bytes de la cadena señalada por src en la cadena señalada por destino . Se admite el solapamiento de la cadena de origen y de destino.
void * memchr(const void *b, int c, size_t num) Esta función busca el unsigned char que se le proporciona en c dentro de la cadena señalada por b . Si el carácter aparece entre los num primeros, se devuelve un puntero del carácter en cuestión. Si no consta entre ellos, se devuele el puntero NULL .
int memcmp(const void *b1, const void *b2, size_t num) Esta función sirve para comparar cadenas, suponiendo que ambas tienen num bytes de longitud. Si las cadenas son iguales byte por byte, la función proporciona el valor cero. En caso contrario, se devuelve la diferencia entre los dos primeros bytes que sean distintos (tomados como unsigned char ). Dos cadenas nulas son siempre idénticas.
void * memcpy(void *destino, const void *src, size_t num) Sirve para copiar num bytes de la cadena señalada por src en la cadena señalada por destino . Si las cadenas se solapan puede haber problemas, así que es preferible utilizar bcopy() .
void * memmove(void *destino, const void *src, size_t num) Sirve para copiar num bytes de la cadena señalada por src en la cadena señalada por destino . Se admite el solapamiento de la cadena de src y de destino. La función proporciona el valor original de destino .
void * memset(void *b, int c, size_t num) Esta función escribe el unsigned char dado en c en la cadena señalada por b ; c se repite tantas veces como se indique en num
char * strcat(char *primera, const char *segunda) Esta función añade la cadena señalada por segunda al final de la cadena señalada por primera , y después añade un '\0' para marcar el fin de cadena. La cadena señalada por primera debe disponer de espacio suficiente para almacenar el resultado de la concatenación.
char * strchr(const char *s, int c) Esta función busca la primera aparición del carácter c en la cadena s . Si lo encuentra, proporciona un puntero del carácter; en caso contrario, proporciona el puntero nulo. Se considera que el '\0' final forma parte de la cadena, y es posible buscarlo dando a c el valor '\0'.
int strcmp(const char *s1, const char *s2) Esta función efectúa una comparación lexicográfica de las cadenas s1 y s2 , que se suponen dotadas de la marca de fin de cadena '\0'. La función proporciona un valor entero mayor, igual o menor que cero según s1 sea mayor, igual o menor que s2 .
int strncmp(const char *s1, const char *s2, size_t num) Esta función efectúa una comparación lexicográfica de las cadenas s1 y s2 , que se suponen dotadas de la marca de fin de cadena '\0'. La función proporciona un valor entero mayor, igual o menor que cero según s1 sea mayor, igual o menor que s2 . La función compara un máximo de num caracteres.
char * strcpy(char *destino, const char *src) Esta función copia la cadena src en la cadena destino , incluyendo el marcador de fin de cadena '\0'.
char * strncpy(char *destino, const char *src, size_t num) Esta función copia la cadena src en la cadena destino , incluyendo el marcador de fin de cadena '\0' si la cadena src contiene menos de num caracteres. Si hay más de num caracteres, no se añade el marcador de fin de cadena.
int strcoll(const char *s1, const char *s2) Esta función proporciona un número positivo, nulo o negativo según s1 sea lexicográficamente manor, igual o menor que s2 . Se respeta la secuencia de clasificación impuesta por el locale aplicado.
size_t strcspn(const char *s, const char *conjunto) Esta función indica la posición del primer carácter de s que no consta en la cadena señalada por conjunto . Se proporciona como resultado el número de caracteres de s , empezando por el principio, que hay que saltar hasta llegar a un carácter que no esté en conjunto .
size_t strlen(const char *s) Esta función proporciona el número de caracteres de que consta la cadena señalada por s .
char * strncat(char *s, const char *append, size_t num) Esta función añade la cadena señalada por segunda al final de la cadena señalada por primera , y después añade un '\0' para marcar el fin de cadena. La cadena señalada por primera debe disponer de espacio suficiente para almacenar el resultado de la concatenación. Como máximo, se añaden num caracteres.
char * strpbrk(const char *s, const char *conjunto) Esta función busca en la cadena señalada por s , que se suponer dotada del carácter de terminación '\0', la primera aparición de un carácter presente en conjunto . Si encuentra uno de estos caracteres, proporciona su puntero; si no lo encuentra, proporciona el puntero NULL .
char * strchr(const char *s, int c) Esta función busca el carácter dado en c en la cadena señalada por s . Si existe, proporciona su puntero; si no existe, proporciona el puntero NULL .
size_t strspn(const char *s, const char *conjunto) Esta función proporciona el número de caracteres, contados desde el principio de s , que se encuentran entre los caracteres indicados en conjunto . Se supone que s está dotada del marcador de fin de cadena, '\0'.
char * strstr(const char *grande, const char *pequeña) Esta función busca la cadena pequeña en la cadena grande . Si la encuentra, proporciona el puntero de pequeña dentro de grande . Si no la encuentra, proporciona el puntero NULL . Si la cadena pequeña está vacía, la función proporciona el puntero grande .
char * strtok(char *str, const char *sep) Esta función va proporcionando uno tras otro los grupos de caracteres separados por algún carácter de los indicados por sep. La primera llamada a strtok() debe aportar la cadena estudiada en el primer argumento; las siguientes deben tener el valor NULL en ese argumento. Cuando no quedan ya más grupos de caracteres, strtok() proporciona el valor NULL . Esta función es ideal para analizar archivos de texto de formato delimitado. Véase también strsep() .
size_t strxfrm(char *destino, const char *src, size_t n) Esta función sirve para transformar la cadena dada por src en una cadena tal que la comparación de esta nueva cadena mediante strcmp() produciría los mismos efectos que strcoll() , respetando por tanto los valores de de LC_COLLATE proporcionados por el locale en vigor.


Ejemplo .- Construir un programa que defina una cadena y un puntero de carácter, y que asigne a este último la dirección de la primera. Verificar las extensiones y asignaciones de las variables mencionadas.
/*
  Este programa muestra el uso de punteros
  elementales de cadenas.
*/


#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char cadena[80] = "Un ejemplo de cadena.";
char * puntero_cadena = cadena;
int i;
int main(void)
{
  printf("Cadenas de caracteres.\n\n");
  printf("Espacio reservado para cadena          : %d\n",sizeof(cadena));
  printf("Espacio ocupado dentro de cadena       : %d\n",strlen(cadena));
  printf("La dirección de cadena es              : %u\n",(unsigned int)cadena);
  printf("El valor de puntero_cadena es          : %u\n",(unsigned int)puntero_cadena);
  printf("El contenido de cadena es              : %s\n",cadena);
  printf("La cadena que señala puntero_cadena es : %s\n", puntero_cadena);
  printf("El cuarto elemento es                  : %c\n", *(cadena+3));
  printf("El cuarto elemento es                  : %c\n", *(puntero_cadena+3));
  printf("El cuarto elemento es                  : %c\n", cadena[3]);
  printf("El cuarto elemento es                  : %c\n", puntero_cadena[3]);
  printf("La cadena contiene                     : %s\n",cadena);

  printf("La cadena contiene                     : ");
  while(*puntero_cadena) printf("%c",*puntero_cadena++);
  printf("\n");

  printf("La cadena contiene                     : ");
  for(i=0;i<strlen(cadena);i++) printf("%c",cadena[i]);
  printf("\n");

  puntero_cadena = cadena; /* Esencial! */
  printf("La cadena contiene                     : ");
  for(i=0;i<strlen(cadena);i++) printf("%c",*(puntero_cadena+i));
  printf("\n");

  printf("\n\nTerminación normal de programa.\n");
  return 0;
}
/*
  RESULTADO

  Cadenas de caracteres.

  Espacio reservado para cadena          : 80
  Espacio ocupado dentro de cadena       : 21
  La dirección de cadena es              : 97292840
  El valor de puntero_cadena es          : 97292840
  El contenido de cadena es              : Un ejemplo de cadena.
  La cadena que señala puntero_cadena es : Un ejemplo de cadena.
  El cuarto elemento es                  : e
  El cuarto elemento es                  : e
  El cuarto elemento es                  : e
  El cuarto elemento es                  : e
  La cadena contiene                     : Un ejemplo de cadena.
  La cadena contiene                     : Un ejemplo de cadena.
  La cadena contiene                     : Un ejemplo de cadena.
  La cadena contiene                     : Un ejemplo de cadena.


  Terminación normal de programa.
*/


Ejercicios propuestos



  1. Ejercicio 0702r01.- Considérese una lista formada por NxPxQ elementos de tipo float. La lista contendrá los números 1.0, 2.0, 3.0, etc. Se pide construir un programa que muestre las direcciones de todos los elementos de la lista, empleando el operador &. N, P y Q son números enteros (por ejemplo 2, 3 y 4).

  2. Ejercicio 0702r02.- Considérese una tabla formada por N filas y PxQ columnas, donde N, P y Q son enteros (por ejemplo 2, 3 y 4). La tabla contendrá los números 1.0. 2.0, 3.0, etc, llenando la tabla por filas. Se pide construir un programa que muestre las direcciones de todos los elementos de la tabla, empleando el operador &. ¿Se observa alguna relación con los resultados del ejercicio anterior? ¿Qué sucede?

  3. Ejercicio 0702r03.- Considérese un tarugo formado por N capas, cada una de las cuales poseerá P filas de Q columnas, siendo N, P y Q números enteros como en el caso anterior (por ejemplo, 2, 3 y 4). Se pide construir un programa que muestre las direciones de todos los elementos del tarugo, empleando el operador &. ¿Se observa alguna relación con los resultados de los ejercicios anteriores? ¿Qué sucede?

  4. Ejercicio 0702r04.- Se dispone de una cadena de caracteres, leída de teclado y correctamente almacenada (con su marcador de fin de cadena). Se pide escribir una función que muestre en pantalla la cadena caráctere a carácter.

  5. Ejercicio 0702r05.- Se dispone de una cadena procedente de un fichero de formato delimitado. Se pide construir una función que proporcione una lista de punteros de cadena, cada uno de los cuales señalará un campo extraído de la cadena original. El número de campos se proporcionará también, a través de un parámetro.

  6. Ejercicio 0702r06.- Se dispone de una cadena procedente de un fichero de formato encolumnado. Se pide construir una función que proporcione una lista de punteros de cadena, cada uno de los cuales señalará un campo extraído de la cadena original. El número de campos se proporcionará a la función a través de un parámetro, así como las extensiones de los mismos (mediante un puntero de int ).

  7. Ejercicio 0702r07.- Se dispone de dos listas de enteros, y se desea conocer la suma de productos de elementos homólogos. Se pide construir una función que calcule este valor.

  8. Ejercicio 0702r08.- Se dispone de dos matrices MxN de float. Se pide construir una función basada en punteros de float que permita calcula la suma de estas dos matrices.