Subsecciones

10. Tópicos avanzados con apuntadores

Se han revisado varias aplicaciones y técnicas que usan apuntadores en los capítulos anteriores. Así mismo se han introducido algunos temas avanzados en el uso de apuntadores. En este capítulo se profundizan algunos tópicos que ya han sido mencionados brevemente y otros que completan la revisión de apuntadores en C.

En este capítulo se desarrolla lo siguiente:

10.1 Apuntadores a apuntadores

Un arreglo de apuntadores es lo mismo que apuntadores a apuntadores. El concepto de arreglos de apuntadores es directo ya que el arreglo mantiene su significado claro. Sin embargo, se pueden confundir los apuntadores a apuntadores.

Un apuntador a un apuntador es una forma de direccionamiento indirecto múltiple, o una cadena de apuntadores. Como se ve en la figura 10.1, en el caso de un apuntador normal, el valor del apuntador es la dirección de la variable que contiene el valor deseado. En el caso de un apuntador a un apuntador, el primer apuntador contiene la dirección del segundo apuntador, que apunta a la variable que contiene el valor deseado.

Se puede llevar direccionamiento indirecto múltiple a cualquier extensión deseada, pero hay pocos casos donde más de un apuntador a un apuntador sea necesario, o incluso bueno de usar. La dirección indirecta en exceso es difícil de seguir y propensa a errores conceptuales.

Se puede tener un apuntador a otro apuntador de cualquier tipo. Considere el siguiente código:

main()
{
    char ch;     /* Un caracter */
    char *pch;   /* Un apuntador a caracter */
    char **ppch; /* Un apuntador a un apuntador a caracter */

    ch   = 'A';
    pch  = &ch;
    ppch = &pch;
    printf("%c\n", **ppch);   /* muestra el valor de ch */
}

Lo anterior se puede visualizar como se muestra en la figura 10.1, en donde se observa que **ppch se refiere a la dirección de memoria de *pch, la cual a su vez se refiere a la dirección de memoria de la variable ch. Pero ¿qué significa lo anterior en la práctica?

Figura 10.1: Apuntador a un apuntador, y apuntador a un char .
\includegraphics[width=3.5in, clip]{figuras/ap1.eps}

Se debe recordar que char * se refiere a una cadena la cual termina con un nulo. Por lo tanto, un uso común y conveniente es declarar un apuntador a un apuntador, y el apuntador a una cadena, ver figura 10.2.

Figura 10.2: Apuntador a un apuntador, y apuntador a una cadena.
\includegraphics[width=3.5in, clip]{figuras/ap2.eps}

Tomando un paso más allá lo anterior, se pueden tener varias cadenas apuntadas por el apuntador, ver figura 10.3

Figura 10.3: Apuntador a varias cadenas.
\includegraphics[width=3.5in, clip]{figuras/ap3.eps}

Se pueden hacer referencias a cadenas individuales mediante ppch[0], ppch[1], .... Esto es idéntico a haber declarado char *ppch[].

Una aplicación común de lo anterior es en los argumentos de la línea de comandos que se revisarán a continuación.

10.2 Entrada en la línea de comandos

C permite leer argumentos en la línea de comandos, los cuales pueden ser usados en los programas.

Los argumentos son dados o tecleados después del nombre del programa al momento de ser ejecutado el programa.

Lo anterior se ha visto al momento de compilar, por ejemplo:

gcc -o prog prog.c

donde gcc es el compilador y -o prog prog.c son los argumentos.

Para poder usar los argumentos en el código se debe definir como sigue la función main.

main(int argc, char **argv)

o

main(int argc, char *argv[])

Con lo que la función principal tiene ahora sus propios argumentos. Estos son solamente los únicos argumentos que la función main acepta.

*
argc es el número de argumentos dados -- incluyendo el nombre del programa.
*
argv es un arreglo de cadenas que tiene a cada uno de los argumentos de la línea de comandos -- incluyendo el nombre del programa en el primer elemento del arreglo.

Se muestra a continuación un programa de ejemplo:

main (int argc, char **argv)
{
    /* Este programa muestra los argumentos de la linea de comandos */
    int i;

    printf("argc = %d\n\n",argc);
    for (i=0; i<argc; ++i)
        printf("\t\targv[%d]: %s\n", i, argv[i]);
}

Suponiendo que se compila y se ejecuta con los siguientes argumentos:

    args f1 "f2 y f3" f4 5 FIN

La salida será:

argc = 6

        argv[0]: args
        argv[1]: f1
        argv[2]: f2 y f3
        argv[3]: f4
        argv[4]: 5
        argv[5]: FIN

Observar lo siguiente:

-
argv[0] contiene el nombre del programa.
-
argc cuenta el número de argumentos incluyendo el nombre del programa.
-
Los espacios en blanco delimitan el fin de los argumentos.
-
Las comillas dobles " " son ignoradas y son usadas para incluir espacios dentro de un argumento.

10.3 Apuntadores a funciones

Los apuntadores a funciones son quizá uno de los usos más confusos de los apuntadores en C. Los apuntadores a funciones no son tan comunes como otros usos que tienen los apuntadores. Sin embargo, un uso común es cuando se pasan apuntadores a funciones como parámetros en la llamada a una función.

Lo anterior es especialmente útil cuando se deben usar distintas funciones quizás para realizar tareas similares con los datos. Por ejemplo, se pueden pasar los datos y la función que será usada por alguna función de control. Como se verá más adelante la biblioteca estándar de C da funciones para ordenamiento (qsort) y para realizar búsqueda (bsearch), a las cuales se les pueden pasar funciones.

Para declarar un apuntador a una función se debe hacer:

int (*pf) ();

Lo cual declara un apuntador pf a una función que regresa un tipo de dato int. Todavía no se ha indicado a que función apunta.

Suponiendo que se tiene una función int f(), entonces simplemente se debe escribir:

pf = &f;

para que $pf$ apunte a la función $f()$.

Para que trabaje en forma completa el compilador es conveniente que se tengan los prototipos completos de las funciones y los apuntadores a las funciones, por ejemplo:

int f(int);
int (*pf) (int) = &f;

Ahora f() regresa un entero y toma un entero como parámetro.

Se pueden hacer cosas como:

ans = f(5);
ans = pf(5);

los cuales son equivalentes.

La función de la biblioteca estándar qsort es muy útil y esta diseñada para ordenar un arreglo usando un valor como llave de cualquier tipo para ordenar en forma ascendente.

El prototipo de la función qsort de la biblioteca stdlib.h es:

    void qsort(void *base, size_t nmiemb, size_t tam,
        int (*compar)(const void *, const void *));

El argumento base apunta al comienzo del vector que será ordenado, nmiemb indica el tamaño del arreglo, tam es el tamaño en bytes de cada elemento del arreglo y el argumento final compar es un apuntador a una función.

La función qsort llama a la función compar la cual es definida por el usuario para comparar los datos cuando se ordenen. Observar que qsort conserva su independencia respecto al tipo de dato al dejarle la responsabilidad al usuario. La función compar debe regresar un determinado valor entero de acuerdo al resultado de comparación, que debe ser:

menor que cero : si el primer valor es menor que el segundo.

cero : si el primer valor es igual que el segundo.

mayor que cero : si el primer valor es mayor que el segundo.

A continuación se muestra un ejemplo que ordena un arreglo de caracteres, observar que en la función comp, se hace un cast para forzar el tipo void * al tipo char *.

#include <stdlib.h>

int comp(const void *i, const void *j);

main()
{
	int i;
	char cad[] = "facultad de ciencias fisico-matematicas";
	
	printf("\n\nArreglo original: \n");
	for (i=0; i<strlen(cad); i++)
		printf("%c", cad[i]);

	qsort(cad, strlen(cad), sizeof(char), comp );

	printf("\n\nArreglo ordenado: \n");
	for (i=0; i<strlen(cad); i++)
		printf("%c", cad[i]);

	printf("\n");
}

int comp(const void *i, const void *j)
{
	char *a, *b;

	a = (char *) i; /* Para forzar void * al tipo char *, se hace cast */ 
	b = (char *) j; /*     empleando (char *)                          */
	return *a - *b;  
}

10.4 Ejercicios

  1. Escribir un programa que muestre las últimas líneas de un texto de entrada. Por defecto o ``default'' n deberá ser 7, pero el programa deberá permitir un argumento opcional tal que
    ultlin n
    muestra las últimas n líneas, donde n es un entero. El programa deberá hacer el mejor uso del espacio de almacenamiento. (El texto de entrada podrá ser leído de un archivo dado desde la línea de comandos o leyendo un archivo de la entrada estándar.)

  2. Escribir un programa que ordene una lista de enteros en forma ascendente. Sin embargo, si una opción r esta presente en la línea de comandos el programa deberá ordenar la lista en forma descendente.

  3. Escribir un programa que lea la siguiente estructura y ordene los datos por la llave usando qsort

    typedef struct {
    	char llave[10];
    	int  algo_mas;
    } Record;