CAPÍTULO 3: Punteros y Cadenas de Texto

El estudio de las cadenas de texto es útil y hace más estrecha la relación entre punteros y arrays. También ilustra de forma fácil cómo puede ser la implementación de algunas funciones de cadenas de texto en C estándar. Finalmente se muestra cómo y cuándo los punteros pueden y deben pasarse como argumentos a una función.

En C, las cadenas son arrays de caracteres (char), pero esto a veces no se cumple en otros lenguajes. En BASIC, Pascal, Fortran y otros lenguajes, una cadena tiene su propio tipo, pero en C no. En C una cadena es un array de caracteres que finaliza con un caracter binario cero (escrito como '\0'). Para comenzar vamos a escribir algo de código que, sólo con fines ilustrativos, probablemente nunca escribirías en un verdadero programa. Consideremos, por ejemplo:

    char mi_cadena[40];

    mi_cadena[0] = 'T';
    mi_cadena[1] = 'e';
    mi_cadena[2] = 'd':
    mi_cadena[3] = '\0';

Uno nunca crearía una cadena así, pero el resultado final es una cadena compuesta por un array de caracteres finalizada por un caracter nul (así lo define C). Hay que ser conscientes de que "nul" no es lo mismo que "NULL". nul se refiere a un cero definido por la secuencia de escape '\0' (ocupa un byte en memoria). NULL, por otra parte, es el nombre de una macro que sirve para inicializar punteros nulos y se define en el archivo de cabecera del compilador de C (nul no puede definirse).

Tardaríamos demasiado tiempo en escribir el código anterior, así que C permite dos formas alternativas para conseguir lo mismo. En primer lugar, podríamos escribir:

    char mi_cadena[40] = {'T', 'e', 'd', '\0',};    

Pero también tardaríamos más de lo conveniente, por eso C permite:

    char mi_cadena[40] = "Ted";

Al utilizar comillas dobles en lugar de comillas simples, el caracter nul ( '\0') se agrega automáticamente al final de la cadena.

En el resto de casos ocurre lo mismo: el compilador reserva un bloque en memoria de 40 bytes para alojar los caracteres e inicializa sus primeros 4 caracteres con Ted\0.

Ahora consideremos el siguiente programa:

------------------programa 3.1------------------------------------

/* Programa 3.1 de PTRTUT10.HTM   6/13/97 */

#include <stdio.h>

char strA[80] = "Una cadena de prueba";
char strB[80];

int main(void)
{
    char *pA;     /* un puntero de tipo caracter */
    char *pB;     /* otro puntero de tipo caracter */
    puts(strA);   /* muestra la cadena strA */
    pA = strA;    /* apunta pA a strA */
    puts(pA);     /* muestra a donde apunta pA */
    pB = strB;    /* apunta pB a strB */
    putchar('\n');       /* salto de línea */
    while(*pA != '\0')   /* línea A (ver texto) */
    {
        *pB++ = *pA++;   /* línea B (ver texto) */
    }
    *pB = '\0';          /* línea C (ver texto) */
    puts(strB);          /* muestra strB */
    return 0;
}

--------- fin programa 3.1 ---------------------------------------

    

En el código anterior comenzamos definiendo dos arrays de 80 caracteres cada uno. Como están definidos de forma global, ambos se inicializan primero con '\0 y después strA se inicializa con los caracteres de la cadena de texto entre comillas.

Ahora, volviendo al código, declaramos dos punteros y mostramos por pantalla la cadena de texto. Entonces "apuntamos" el puntero pA a strA, es decir, por medio de la instrucción de asignación se copia la dirección de strA[0] en nuestra variable pA y utilizamos puts() para mostrar por pantalla donde apunta pA. Consideremos ahora que el prototipo de la función puts() es:

    int puts(const char *s); 

Por ahora ignoremos const. El parámetro pasado a puts() es un puntero, es decir, el valor de un puntero (ya que todos los parámetros en C se pasan por valor), y el valor de un puntero es la dirección a donde apunta, o, simplemente una dirección. Así, cuando escribimos puts(strA); como hemos visto, estamos pasando la dirección de strA[0].

Del mismo modo, cuando escribimos puts(pA); estamos pasando la misma dirección, ya que hemos realizado la asignación pA = strA;

Teniendo en cuenta todo esto, bajamos hasta la sentencia while() en la línea A. La línea A dice:

Mientras el caracter al que apunta pA (es decir *pA) no sea un caracter nul (es decir '\0'), haz lo siguiente:

La línea B dice: copia el caracter apuntado por pA al espacio donde apunta pB, y luego incrementa pA para que apunte al siguiente caracter y pB para que apunte al siguiente espacio.

Cuando se copia el último caracter, pA apunta al caracter de terminación nul y finaliza el bucle. Sin embargo no se ha copiado el caracter nul y como en C, por definición, una cadena de texto debe finalizar con nul, lo agregamos con la línea C.

Es muy educativo ejecutar este programa con el depurador para ver strA, strB, pA y pB paso a paso. Incluso es más educativo definir strB[] con algo como:

    strB[80] = "12345678901234567890123456789012345678901234567890"

donde el número de dígitos utilizados es mayor que la longitud de strA y repetir la depuración paso a paso viendo las variables anteriores. ¡Pruébalo!

Volviendo al prototipo de puts() por un momento, al utilizar "const" como un modificador del parámetro indica al usuario que la función no modificará la cadena apuntada por s, es decir, se tratará a esa cadena como una constante.

Por supuesto, lo que muestra el programa anterior es una forma simple de copiar una cadena de texto. Después de que comprendas bien lo que sucede jugando con lo anterior, podemos crear nuestra propia versión de la función strcpy() estándar de C, que podría ser:

   char *mi_copiacadena(char *destino, char *origen)
   {
       char *p = destino;
       while (*origen != '\0')
       {
           *p++ = *origen++;
       }
       *p = '\0';
       return destino;
   }   

En este caso, he seguido la práctica utilizada en la rutina estándar de devolver un puntero a la cadena de destino.

De nuevo, la función está diseñada para aceptar dos valores de dos punteros de tipo caracter (char), es decir, direcciones, por lo que en el programa anterior podríamos escribir:

    int main(void)
    {
        mi_copiacadena(strB, strA);
        puts(strB);
    }    

Me he desviado ligeramente de la forma utilizada en C estándar que tendría el prototipo:

    char *mi_copiacadena(char *destino, const char *origen);  

Aquí, el modificador "const" se utiliza para asegurar que la función no modificará el contenido donde apunta el puntero origen. Puedes probarlo modificando la función anterior y su prototipo para incluir el modificador "const" como indica. Después, dentro de la función puedes agregar una declaración que cambie el contenido donde apunta el puntero origen, como por ejemplo:

    *origen = 'X';

que cambiaría el primer caracter de la cadena con una X. El modificador const debería provocar un error al compilar el programa. Pruébalo y verás.

Ahora, consideremos algunas de las cosas que hemos visto en ejemplos anteriores. En primer lugar, hay que tener en cuenta el hecho de que *ptr++ debe ser interpretado como el valor donde apunta ptr y después el incremento del valor del puntero. Esto tiene que ver con la precedencia de los operadores. Si escribiéramos (*ptr)++ no estaríamos incrementado el puntero, sino el valor donde apunta, es decir, si se utiliza en el primer caracter del ejemplo anterior ("Ted"), 'T' se incrementaría en uno, es decir: 'U'. Puedes escribir algún ejemplo simple para verlo.

Recordemos una vez más que una cadena de texto no es más que una serie de caracteres donde el último caracter es un '\0'. Lo que hemos hecho antes es copiar un array de caracteres, pero la técnica se puede aplicar a un array de enteros, floats, etc. En esos casos, sin embargo, al no tratarse de cadenas de texto, el final del array no acabaría con ningún caracter especial como nul. Podríamos implementar una versión basada en un valor especial para identificar el final del array, por ejemplo: copiar un array de enteros positivos marcando el final con un entero negativo. Lo normal cuando escribimos una función para copiar un array de elementos que no sean una cadena de texto es pasar un argumento más con el número de elementos a copiar, así como otro argumento con la dirección del array, es decir, algo parecido al siguiente prototipo:

    void int_copiar(int *ptrA, int *ptrB, int nbr);

donde nbr es el número de enteros a copiar. Puedes jugar con esta idea, crear un array de enteros, ver si puedes escribir la función int_copiar y hacerla funcionar.

Esto permite utilizar funciones para manipular grandes arrays. Por ejemplo, si tenemos un array de 5000 enteros y queremos manipularlo con una función, sólo necesitamos pasar a esta función la dirección del array (y alguna información auxiliar como el entero nbr anterior, dependiendo de lo que necesites). El array en sí no se pasa a la función, es decir, no se copia todo el array y se pone en la pila antes de llamar a la función, sólo se pasa su dirección.

Otra cosa distinta es pasar, digamos un entero a una función. Cuando pasamos un entero hacemos una copia del entero, es decir, ponemos su valor en la pila. Cualquier manipulación del valor pasado dentro de la función no afecta al entero original, pero con arrays y punteros podemos pasar la dirección de la variable y manipular los valores de las variables originales.

Continuar con el Tutorial de Punteros

Tabla de Contenidos