← Programación en C para Sistemas Operativos Bloque 4 · Punteros · Tema 19 de 42

Tema 19

Punteros a punteros y const

Un puntero es una variable, y como toda variable tiene una dirección: se puede tener un puntero que apunte a otro puntero. Parece un juego de palabras, pero es imprescindible en la programación de sistemas. argv es un char **, igual que el entorno del proceso; strtol y getline reciben punteros a punteros para poder modificar los punteros del llamador. Con dos niveles de punteros aparece también una pregunta que con uno solo pasaba inadvertida: cuando una declaración tiene const, ¿qué es lo constante, el dato, el puntero, o el puntero al puntero? Este tema responde las dos cosas.

1. Un puntero a un puntero

Un puntero a un int se declara int *p. Un puntero a un puntero a un int se declara con dos asteriscos, int **pp, y guarda la dirección de una variable de tipo int *:

niveles.c
#include <stdio.h>

int main(void)
{
    int x = 7;
    int *p = &x;
    int **pp = &p;

    printf("x   = %d,              &x  = %p\n", x, (void *) &x);
    printf("p   = %p, &p  = %p\n", (void *) p, (void *) &p);
    printf("pp  = %p, &pp = %p\n", (void *) pp, (void *) &pp);
    printf("*pp = %p   (es p)\n", (void *) *pp);
    printf("**pp = %d               (es x)\n", **pp);

    **pp = 8;                       /* modifica x */
    printf("tras **pp = 8: x = %d\n", x);

    int y = 100;
    *pp = &y;                       /* modifica p: ahora apunta a y */
    printf("tras *pp = &y: *p = %d\n", *p);
    return 0;
}
terminal
$ ./niveles
x   = 7,              &x  = 0x7fff494efde0
p   = 0x7fff494efde0, &p  = 0x7fff494efde8
pp  = 0x7fff494efde8, &pp = 0x7fff494efdf0
*pp = 0x7fff494efde0   (es p)
**pp = 7               (es x)
tras **pp = 8: x = 8
tras *pp = &y: *p = 100
Una cadena de punteros: pp, en 0x...df0, guarda la dirección de p, 0x...de8; p, en 0x...de8, guarda la dirección de x, 0x...de0; x, en 0x...de0, vale 7. *pp es p, y **pp es x pp int ** · …df0 …de8 p int * · …de8 …de0 x int · …de0 7 pp *pp **pp Cada asterisco sigue una flecha: *pp es la variable p, y **pp es la variable x.
Tres variables en la pila, cada una a 8 bytes de la anterior. pp no sabe nada de x: solo sabe dónde está p.

Las tres variables están en la pila, y la salida permite seguir la cadena: pp contiene la dirección de p, y p la de x. Como en el tema 15, cada * «sigue una flecha»: *pp es p, y **pp es x. A través de pp se puede modificar cualquiera de los dos: **pp = 8 cambia el dato, x; *pp = &y cambia el puntero intermedio, p, que pasa a apuntar a otra variable. Esa segunda posibilidad es la que justifica los punteros a punteros.

2. Los niveles de indirección

A cada asterisco se lo llama un nivel de indirección: la cantidad de veces que hay que seguir una dirección para llegar al dato. Con int **pp, cada expresión tiene un tipo distinto, y conviene saber leerlos:

ExpresiónTipoEsTamaño
ppint **La dirección de p.8
*ppint *La variable p: la dirección de x.8
**ppintLa variable x.4
&ppint ***La dirección de pp.8

Una regla práctica para no perderse: el tipo de una expresión tiene tantos asteriscos como el de la variable, menos uno por cada * aplicado, más uno por cada &. Si una asignación no cuadra, como guardar un int * en un int **, el compilador lo rechaza con incompatible pointer type, y contar asteriscos suele mostrar el error. Todos los punteros, de cualquier nivel, ocupan 8 bytes en Linux de 64 bits.

3. Modificar un puntero del llamador

En el tema 16 vimos que, para que una función modifique una variable del llamador, hay que pasarle su dirección. Si la variable que hay que modificar es un puntero, su dirección es un puntero a puntero. Es el caso de una función que recorre un texto y tiene que dejarle al llamador el lugar donde se quedó, para continuar desde ahí en la llamada siguiente:

palabras.c
#include <stdio.h>

/*
 * Busca la siguiente palabra a partir de *cursor. Si la encuentra, deja en
 * *inicio y *largo dónde está, avanza *cursor hasta después de ella y
 * devuelve 1. Si no quedan palabras, devuelve 0.
 */
static int siguiente_palabra(const char **cursor, const char **inicio, size_t *largo)
{
    const char *p = *cursor;

    while (*p == ' ' || *p == '\t')         /* saltar espacios */
        p++;
    if (*p == '\0')
        return 0;
    *inicio = p;
    while (*p != '\0' && *p != ' ' && *p != '\t')
        p++;
    *largo = (size_t) (p - *inicio);
    *cursor = p;                            /* el llamador sigue desde aquí */
    return 1;
}

int main(void)
{
    const char *orden = "  gcc  -Wall -o hola   hola.c ";
    const char *cursor = orden;
    const char *palabra;
    size_t largo;
    int n = 0;

    while (siguiente_palabra(&cursor, &palabra, &largo))
        printf("palabra %d: \"%.*s\" (posición %td)\n", ++n, (int) largo, palabra, palabra - orden);
    return 0;
}
terminal
$ ./palabras
palabra 1: "gcc" (posición 2)
palabra 2: "-Wall" (posición 7)
palabra 3: "-o" (posición 13)
palabra 4: "hola" (posición 16)
palabra 5: "hola.c" (posición 23)

La función tiene dos parámetros de tipo const char **, con papeles distintos. cursor es de entrada y salida, el caso de la sección: la función lee *cursor para saber desde dónde empezar, y al final escribe en *cursor la nueva posición, de modo que el puntero de main avanza de palabra en palabra entre una llamada y otra. inicio es de salida: la función escribe en *inicio un puntero al comienzo de la palabra, dentro del mismo texto. Ninguno de los dos copia nada: son punteros que señalan posiciones de la cadena original, que se delimitan como en el tema 18, con un comienzo y un largo.

Es el mismo diseño que usa la biblioteca. strtol(texto, &fin, 10), que vimos en el tema 10, recibe un char ** y deja en fin la posición donde terminó el número. getline(&linea, &capacidad, stdin) recibe un char ** porque puede tener que reemplazar el buffer por uno más grande, y entonces necesita cambiar el puntero linea del llamador. Y strtok_r, del tema 22, usa un char ** exactamente como nuestro cursor.

4. argv y environ

Los dos char ** más importantes de cualquier programa son los que recibe al arrancar: los argumentos y el entorno. Los dos tienen la misma forma, un arreglo de punteros a cadenas terminado con un puntero NULL:

argumentos.c
#include <stdio.h>
#include <string.h>

extern char **environ;               /* las variables de entorno, de POSIX */

int main(int argc, char *argv[])
{
    printf("argc = %d, sizeof argv = %zu (es un char **)\n", argc, sizeof argv);

    /* recorrer argv con un puntero a puntero, hasta el NULL final */
    for (char **p = argv; *p != NULL; p++)
        printf("  argv[%td] = %s\n", p - argv, *p);
    printf("  argv[argc] = %p\n", (void *) argv[argc]);

    /* environ tiene la misma forma: punteros a cadenas, terminado en NULL */
    int cantidad = 0;
    const char *ruta = NULL;
    for (char **e = environ; *e != NULL; e++) {
        cantidad++;
        if (strncmp(*e, "PATH=", 5) == 0)
            ruta = *e + 5;
    }
    printf("%d variables de entorno; PATH = %.40s...\n", cantidad, ruta ? ruta : "(no está)");
    return 0;
}
terminal
$ gcc -std=c17 -Wall -Wextra -o argumentos argumentos.c
argumentos.c: In function ‘main’:
argumentos.c:8:75: warning: ‘sizeof’ on array function parameter ‘argv’ will return size of ‘char **’ [-Wsizeof-array-argument]
…
$ ./argumentos uno "dos tres" -v
argc = 4, sizeof argv = 8 (es un char **)
  argv[0] = ./argumentos
  argv[1] = uno
  argv[2] = dos tres
  argv[3] = -v
  argv[argc] = (nil)
22 variables de entorno; PATH = /usr/local/sbin:/usr/local/bin:/usr/sbin...
La estructura de argv: argv es un char ** que apunta al primer elemento de un arreglo de punteros; argv[0] apunta a la cadena ./argumentos, argv[1] a uno, argv[2] a dos tres, argv[3] a -v, y argv[4], que es argv[argc], vale NULL argv char ** argv[0] argv[1] argv[2] argv[3] argv[4] = NULL ./argumentos\0 uno\0 dos tres\0 -v\0 Un arreglo de punteros, cada uno a una cadena, y un NULL al final que marca dónde termina.
Lo que recibe main: un puntero al primero de una fila de punteros. El núcleo arma todo esto en la pila del proceso al ejecutar el programa.

Aunque main declare char *argv[], argv es un char **, por la regla de los parámetros arreglo del tema 17: el compilador lo advierte en el sizeof, que da 8. Apunta al primer elemento de un arreglo de punteros, y cada uno de ellos apunta a una cadena. El arreglo termina con un NULL, argv[argc], que el estándar garantiza, y por eso se puede recorrer con un char ** que avanza hasta encontrarlo, sin usar argc. El texto "dos tres", que la shell recibió entre comillas, llega como un solo argumento con su espacio.

environ, una variable global que define POSIX, tiene la misma forma: punteros a cadenas de la forma NOMBRE=valor, terminados en NULL. Recorrerla es lo que hacen env y getenv. Y *e + 5 combina los dos niveles: *e es la cadena, y sumarle 5 la avanza más allá de "PATH=", la aritmética del tema 18. Esta estructura, arreglo de punteros terminado en NULL, es la que el núcleo construye en la pila de cada proceso nuevo, y la que hay que armar para ejecutar otro programa con execv, en el curso Procesos e Hilos. El tema 23 trata argv en detalle.

5. const y los punteros: cuatro combinaciones

Con una variable común, const tiene un solo significado: la variable no se puede modificar. Con un puntero hay dos cosas que podrían ser constantes: el dato al que apunta, y el propio puntero. Según dónde se escriba const, se protege una, la otra, o las dos:

formas.c
int main(void)
{
    int a = 1, b = 2;

    const int *p1 = &a;          /* puntero a int constante */
    int *const p2 = &a;          /* puntero constante a int */
    const int *const p3 = &a;    /* puntero constante a int constante */

    p1 = &b;                     /* bien: el puntero puede cambiar */
    *p1 = 5;                     /* error: el dato no */

    *p2 = 5;                     /* bien: el dato puede cambiar */
    p2 = &b;                     /* error: el puntero no */

    *p3 = 5;                     /* error */
    p3 = &b;                     /* error */
    return a + b;
}
terminal
$ gcc -std=c17 -Wall -Wextra -c formas.c
formas.c: In function ‘main’:
formas.c:10:9: error: assignment of read-only location ‘*p1’
   10 |     *p1 = 5;                     /* error: el dato no */
      |         ^
formas.c:13:8: error: assignment of read-only variable ‘p2’
   13 |     p2 = &b;                     /* error: el puntero no */
      |        ^
formas.c:15:9: error: assignment of read-only location ‘*(const int *)p3’
   15 |     *p3 = 5;                     /* error */
      |         ^
formas.c:16:8: error: assignment of read-only variable ‘p3’
   16 |     p3 = &b;                     /* error */
      |        ^
DeclaraciónSe lee*p = …p = …Uso típico
int *ppuntero a intsísíLo habitual.
const int *ppuntero a int constantenosíParámetros de entrada: strlen(const char *). El más común.
int *const ppuntero constante a intsínoUn puntero que no debe cambiar de destino. Poco usado.
const int *const ppuntero constante a int constantenonoTablas fijas de punteros a datos fijos.

Los mensajes del compilador distinguen los dos casos con precisión: «read-only location *p1» cuando lo protegido es el lugar apuntado, y «read-only variable p2» cuando lo protegido es la variable puntero. Un detalle importante: const int *p1 = &a no vuelve constante a a, que sigue pudiendo modificarse directamente. const en un puntero dice qué se puede hacer a través de ese puntero: es una vista de solo lectura sobre un dato que otros pueden cambiar.

6. Cómo leer una declaración

Hay una regla que resuelve cualquier declaración con punteros y const: leerla de derecha a izquierda, empezando por el nombre, y pronunciando * como «puntero a». const se aplica a lo que tiene inmediatamente a su izquierda; si no hay nada a su izquierda, a lo que tiene a su derecha.

DeclaraciónLeída de derecha a izquierda
const char *ss es un puntero a char constante.
char const *ss es un puntero a constante char: lo mismo que la anterior.
char *const ss es una constante puntero a char.
char **pppp es un puntero a puntero a char.
const char **pppp es un puntero a puntero a char constante.
char *const *pppp es un puntero a constante puntero a char.
const char *const *pppp es un puntero a constante puntero a char constante.

Las dos primeras filas muestran que const char y char const son lo mismo: el orden entre el tipo y const no importa, lo que importa es de qué lado del asterisco está. Algunas guías de estilo prefieren escribir siempre const a la derecha de lo que protege, char const *, para que la regla se aplique sin excepciones; en el curso, como en la mayoría del código de C, usamos const char *.

7. Agregar y quitar const

Las conversiones entre punteros con y sin const siguen una regla de sentido común: se puede agregar una restricción, nunca quitarla sin decirlo explícitamente.

conversiones.c
int main(void)
{
    int x = 1;
    const int cx = 2;

    int *p = &x;
    const int *cp = p;            /* agregar const: siempre permitido */
    int *q = &cx;                 /* quitar const: no */
    int *r = (int *) cp;          /* con un cast, el compilador calla */

    char *palabras[] = {"uno", "dos"};
    char **pp = palabras;
    const char **cpp = pp;        /* parece agregar const... */
    return *q + *r + (cpp != 0);
}
terminal
$ gcc -std=c17 -Wall -Wextra -c conversiones.c
conversiones.c: In function ‘main’:
conversiones.c:8:14: warning: initialization discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
    8 |     int *q = &cx;                 /* quitar const: no */
      |              ^
conversiones.c:13:24: error: initialization of ‘const char **’ from incompatible pointer type ‘char **’ [-Wincompatible-pointer-types]
   13 |     const char **cpp = pp;        /* parece agregar const... */
      |                        ^~

Pasar de int * a const int * es automático, y es lo que ocurre cada vez que se pasa un dato a una función con un parámetro const: la función recibe una vista de solo lectura. Al revés, de const int * a int *, el compilador advierte que se «descarta el calificador const», porque el puntero resultante permitiría modificar un dato que se había prometido no tocar. Con un cast el compilador calla, pero el dato no se vuelve modificable: si era una constante de verdad, como en trampa.c del tema 6, modificarlo es comportamiento indefinido.

La última línea es la sorpresa del tema: char ** a const char ** parece agregar una restricción, y sin embargo es un error de compilación. La sección siguiente explica por qué.

8. Por qué char ** no se convierte en const char **

Si esa conversión estuviera permitida, abriría un agujero por el que se podría modificar un dato constante sin un solo cast sospechoso a la vista. Este programa lo demuestra, usando un cast para forzar justamente la conversión que el compilador prohíbe:

agujero.c
#include <stdio.h>

int main(void)
{
    const char constante = 'A';     /* no debería cambiar nunca */
    char *p;
    const char **pp = (const char **) &p;   /* lo que el compilador impide sin cast */

    *pp = &constante;               /* p apunta ahora al dato constante */
    *p = 'Z';                       /* y se modifica sin ningún const en el camino */
    printf("constante = %c\n", constante);
    return 0;
}
terminal
$ ./agujero
constante = Z

Paso a paso: pp apunta a p, pero dice que p es un const char *. Por eso *pp = &constante es una asignación perfectamente válida para el compilador: guarda la dirección de un const char en un const char *. Pero esa variable es en realidad p, un char * sin const, y a través de ella *p = 'Z' modifica la constante. Ninguna de las dos últimas líneas tiene nada de sospechoso; el problema estaba en la conversión de la línea anterior. Por eso el estándar la prohíbe. La regla de C es estricta: la conversión implícita solo puede agregar const en el primer nivel, al tipo apuntado directamente. Un char ** se convierte solo en char *const *, donde lo que se vuelve constante es el puntero intermedio, y la asignación *pp = &constante sería imposible. Ni siquiera const char *const *, que cerraría el agujero, se acepta sin cast:

terminal · lista.c: pasar argv a mostrar_c(char *const *) y a mostrar_cc(const char *const *)
$ gcc -std=c17 -Wall -Wextra -o lista lista.c
lista.c: In function ‘main’:
lista.c:19:16: error: passing argument 1 of ‘mostrar_cc’ from incompatible pointer type [-Wincompatible-pointer-types]
   19 |     mostrar_cc(argv);
      |                ^~~~
      |                |
      |                char **
lista.c:3:43: note: expected ‘const char * const*’ but argument is of type ‘char **’

La llamada a mostrar_c, con char *const *, compiló sin quejas; la de mostrar_cc es un error, también con -std=c23. Es una diferencia conocida con C++, que sí acepta esa conversión porque comprobó que es segura; en C, la regla nunca se amplió. En la práctica, el caso aparece al pasar argv, un char **, a una función que recibe una lista de cadenas. Hay dos soluciones: declarar el parámetro char *const *, como hace execv, que acepta argv directamente y protege los punteros de la lista, aunque no las cadenas; o declararlo const char *const *, que protege todo, y hacer un cast explícito en la llamada, sabiendo que esa conversión es segura.

9. const en la biblioteca

Los prototipos de la biblioteca de C son la mejor guía de cómo usar const, y también muestran sus límites históricos:

terminal · tres prototipos de glibc
$ grep -n -A1 'strtol (' /usr/include/stdlib.h | head -n 2
181:extern long int strtol (const char *__restrict __nptr,
182-			char **__restrict __endptr, int __base)
$ grep -n -A1 'execv (' /usr/include/unistd.h
584:extern int execv (const char *__path, char *const __argv[])
585-     __THROW __nonnull ((1, 2));

strtol recibe el texto como const char *, porque no lo modifica, pero el puntero de fin como char **, sin const, aunque apunte a ese mismo texto. Es una concesión del estándar de 1989: con const char **, la función no aceptaría la dirección de un char *, por el agujero de la sección anterior. execv recibe los argumentos como char *const argv[]: promete no modificar los punteros del arreglo, pero no las cadenas, para poder aceptar un char ** como argv sin casts.

El caso más conocido es strchr, que busca un carácter en una cadena. Hasta C17, su prototipo es char *strchr(const char *s, int c): recibe un const char * y devuelve un char * dentro de la misma cadena, de modo que «quita» el const sin que nadie lo note. C23 corrigió el agujero, y glibc lo implementa:

terminal · strchr.c: char *barra = strchr(ruta, '/'), con const char *ruta
$ gcc -std=c17 -Wall -Wextra -o strchr strchr.c && ./strchr
/usr/bin/gcc
$ gcc -std=c23 -Wall -Wextra -o strchr strchr.c
strchr.c: In function ‘main’:
strchr.c:7:19: warning: initialization discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
    7 |     char *barra = strchr(ruta, '/');      /* strchr devuelve char *... */
      |                   ^~~~~~

En C23, strchr, strrchr, strstr y otras funciones de búsqueda devuelven const char * cuando reciben un const char *, y char * cuando reciben un char *. El mismo código que compilaba limpio con C17 ahora advierte, porque efectivamente obtenía un puntero modificable a una cadena que no lo era. La forma correcta, válida en ambas versiones, es const char *barra = strchr(ruta, '/');, como hace ultimo_componente del tema 16.

10. const con dos niveles

Con un puntero a puntero, const puede ir en tres lugares, y cada uno protege un nivel distinto. Tomando como ejemplo un arreglo de cadenas recorrido con un char **:

Declaración¿**pp = 'x'?¿*pp = otra?¿pp++?Ejemplo
char **ppsísísíargv, strtol
const char **ppnosísísiguiente_palabra: cambia los punteros, no los textos.
char *const *ppsínosíexecv
const char *const *ppnonosíUna función que solo lee una lista de cadenas.
const char *const *const ppnononoRaro: el propio pp fijo.

Para una función que recibe una lista de cadenas y solo la lee, como una que muestra los argumentos, el tipo más preciso es const char *const *: no modifica ni los textos ni los punteros. Pero, como vimos en la sección 8, en C no acepta un char ** sin cast; por eso la biblioteca, en execv, prefiere char *const *, que sí lo acepta. Para una función como siguiente_palabra, que tiene que modificar el puntero del llamador pero no el texto, el tipo es const char **, y el llamador debe tener un const char *, como cursor en palabras.c.

11. Tres niveles y más

Nada impide char *** o más asteriscos, y todos ocupan 8 bytes. Aparecen en casos concretos: una función que tiene que crear una lista de cadenas y entregársela al llamador recibe un char ***, porque necesita modificar un char ** del llamador, por la misma lógica de la sección 3. Pero cada nivel extra hace el código más difícil de leer y de corregir. Cuando aparecen tres niveles, suele ser señal de que conviene agrupar los datos en una estructura, como veremos en el bloque 6, y pasar un puntero a ella. Una frase que circula entre programadores de C dice que alguien que escribe *** es un «programador de tres estrellas», y no es un elogio.

12. Punteros a punteros en gdb

gdb sigue los niveles igual que C. Sobre niveles, antes de **pp = 8, y sobre argumentos, al entrar en main:

terminal · gdb sobre niveles y sobre argumentos
$ gdb -q -batch -ex 'break 15' -ex run -ex 'p pp' -ex 'p *pp' -ex 'p **pp' -ex 'ptype pp' ./niveles
Breakpoint 1, main () at niveles.c:15
15	    **pp = 8;                       /* modifica x */
$1 = (int **) 0x7fffffffd7a8
$2 = (int *) 0x7fffffffd7a0
$3 = 7
type = int **
$ gdb -q -batch -ex 'break 8' -ex 'run uno dos' -ex 'p *argv@argc' -ex 'p argv[argc]' -ex 'ptype argv' ./argumentos
Breakpoint 1, main (argc=3, argv=0x7fffffffd8e8) at argumentos.c:8
8	    printf("argc = %d, sizeof argv = %zu (es un char **)\n", argc, sizeof argv);
$1 = {0x7fffffffdb60 "/home/ana/c/tema19/argumentos", 0x7fffffffdb80 "uno", 0x7fffffffdb84 "dos"}
$2 = 0x0
type = char **

Con p *argv@argc, el operador @ del tema 17 muestra los argc punteros del arreglo, y gdb muestra junto a cada dirección la cadena a la que apunta. Las direcciones de las cadenas están muy cerca unas de otras, en la parte alta de la pila, donde el núcleo las copió al crear el proceso, y argv[argc] vale 0x0, el NULL final. ptype argv confirma que, dentro de main, es un char **.

13. Dónde poner const

Con todo lo visto, las pautas para usar const con punteros caben en una lista corta:

  1. En todo parámetro puntero cuyos datos la función solo lee: const T *. Es el uso más importante, y el que más errores evita. entrada
  2. En todo puntero a una cadena literal: const char *. El compilador impide entonces modificarla. literales
  3. En listas de cadenas que una función solo lee: char *const * si debe aceptar argv directamente, como execv; const char *const * si se prefiere proteger también las cadenas, con un cast al pasar un char **. listas
  4. En los punteros que una función devuelve dentro de datos const: devolver const T *, como ultimo_componente y el strchr de C23. retorno
  5. No en los parámetros que se pasan por valor: int f(const int x) no promete nada al llamador (tema 11). por valor
  6. Nunca quitarlo con un cast para modificar el dato: si hay que modificarlo, el tipo estaba mal elegido. casts

14. Errores frecuentes

ErrorQué pasaCorrección
Pasar p donde se espera &p (un T **)La función no puede cambiar el puntero del llamador; error de tipos.Pasar la dirección del puntero: &p.
Confundir *pp con **ppSe modifica el puntero en lugar del dato, o al revés.Contar los niveles: cada * sigue una flecha.
Recorrer argv o environ sin detenerse en NULLSe desreferencia un puntero nulo.for (p = argv; *p != NULL; p++)
Confundir const int *p con int *const pSe protege lo que no se quería.Leer de derecha a izquierda.
Creer que const int *p = &a vuelve constante a aa puede cambiar por otro camino.const en un puntero es una vista de solo lectura.
Quitar const con un cast para modificarComportamiento indefinido si el dato era constante.Revisar los tipos.
Pasar char ** a un parámetro const char ** o const char *const *Error: tipos incompatibles. En C solo se agrega const en el primer nivel.Declarar el parámetro char *const *, o hacer un cast a const char *const *.
Guardar el resultado de strchr sobre una cadena const en un char *Puntero modificable a datos que no lo son; advertencia en C23.const char *.
Usar tres o más niveles sin necesidadCódigo difícil de seguir.Agrupar en una estructura.

15. Resumen

16. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. Con int x = 7; int *p = &x; int **pp = &p;, ¿qué son pp, *pp y **pp, y de qué tipo?

pp es la dirección de p, de tipo int **. *pp es la variable p, de tipo int *, cuyo valor es la dirección de x. **pp es la variable x, de tipo int, que vale 7.

2. ¿Qué diferencia hay entre **pp = 8 y *pp = &y?

**pp = 8 modifica el dato final, x. *pp = &y modifica el puntero intermedio, p, que pasa a apuntar a y; x no cambia. Poder modificar el puntero intermedio es la razón de ser de los punteros a punteros.

3. ¿Por qué siguiente_palabra recibe const char **cursor y no const char *cursor?

Porque tiene que modificar el puntero cursor de main, para que la próxima llamada continúe donde terminó la anterior. Para modificar una variable del llamador hay que recibir su dirección, y la dirección de un const char * es un const char **. Con const char *, la función solo modificaría su copia.

4. ¿Por qué se puede recorrer argv sin usar argc?

Porque el estándar garantiza que argv[argc] es un puntero nulo: el arreglo de punteros termina con un NULL. Un char ** que avanza hasta encontrar *p == NULL recorre todos los argumentos. environ tiene la misma terminación.

5. ¿Qué protege cada uno: const int *p, int *const p?

const int *p es un puntero a int constante: no se puede modificar el dato a través de p, pero p puede apuntar a otro lado. int *const p es un puntero constante: p no puede cambiar de destino, pero el dato sí se puede modificar a través de él.

6. Si const int *p = &a;, ¿puede cambiar el valor de a?

Sí. const en el puntero solo impide modificar a a través de p. La variable a no es constante, y se puede modificar directamente o por otro puntero, y el cambio se verá al leer *p.

7. ¿Cómo se lee char *const *pp?

De derecha a izquierda: pp es un puntero a constante puntero a char. Es decir, se puede modificar pp y los caracteres finales, pero no los punteros intermedios, *pp. Es el tipo del parámetro argv de execv.

8. ¿Por qué int *q = &cx;, con const int cx, produce una advertencia, y const int *cp = p; no?

La primera quita el const: el puntero resultante permitiría modificar una constante, y el compilador advierte que se descarta el calificador. La segunda lo agrega: crea una vista de solo lectura sobre un dato modificable, lo que nunca es peligroso.

9. Explica con agujero.c por qué char ** no se convierte en const char **.

Si se permitiera, un const char **pp podría apuntar a un char *p. Entonces *pp = &constante sería válido, porque guarda la dirección de un const char en algo que dice ser un const char *, pero en realidad la guarda en p, un char *, y *p = 'Z' modificaría la constante sin ningún cast visible. El programa, forzando la conversión con un cast, mostró constante = Z.

10. ¿Por qué strtol recibe char **endptr sin const, si no modifica el texto?

Porque el puntero de fin señala dentro del texto, que la función recibe como const char *, pero si el parámetro fuera const char **, los programas no podrían pasar la dirección de un char * sin un cast, por la regla de la sección 8. El estándar de 1989 eligió la comodidad del llamador a costa de un const perdido.

11. ¿Qué cambió en strchr con C23?

Hasta C17 devolvía siempre char *, aunque recibiera un const char *, de modo que permitía obtener un puntero modificable a una cadena constante sin advertencias. En C23 conserva el calificador: con un const char * devuelve un const char *, y guardar el resultado en un char * produce una advertencia.

12. ¿Qué tipo conviene para una función que recibe una lista de cadenas y solo la muestra, si se la va a llamar con argv?

Hay dos opciones. char *const * acepta argv sin cast y promete no modificar los punteros de la lista, aunque no las cadenas; es lo que hace execv. const char *const * promete no modificar nada, pero en C un char ** no se convierte solo en ese tipo, y la llamada necesita un cast explícito, que en este caso es seguro.

Para practicar

  1. Escribe void mostrar_lista(char *const *lista), que muestre una lista de cadenas terminada en NULL, y úsala con argv y con environ sin ningún cast. Luego cambia el parámetro a const char *const * y observa qué pide el compilador.
  2. Escribe int separar(const char **cursor, char separador, const char **campo, size_t *largo), que extraiga campos separados por un carácter, y úsala para mostrar los campos de una línea de /etc/passwd separados por ':'.
  3. Escribe una función void avanzar_espacios(const char **p) y otra const char *saltar_espacios(const char *p), que hagan lo mismo de dos formas. ¿Cuál resulta más cómoda de usar en un bucle?
  4. Recorre environ y muestra solo las variables cuyo nombre empiece con "LC_" o sea "LANG", mostrando el nombre y el valor por separado con %.*s.
  5. Declara las cuatro combinaciones de const con un char * y, para cada una, escribe una asignación al dato y una al puntero. Antes de compilar, predice cuáles darán error.
  6. Compila strchr.c con -std=c17 y con -std=c23, y corrígelo para que compile sin advertencias en ambas versiones. Haz lo mismo usando strrchr y strstr.
  7. En gdb, detén palabras dentro de siguiente_palabra y muestra cursor, *cursor y **cursor. Usa up para pasar al marco de main y comprueba que &cursor coincide con el cursor de la función.