¿Por qué usar doble indirección? o ¿Por qué usar punteros a punteros?

15 minutos de lectura

avatar de usuario
manju

¿Cuándo se debe usar una doble dirección indirecta en C? ¿Alguien puede explicar con un ejemplo?

Lo que sé es que un doble direccionamiento indirecto es un puntero a un puntero. ¿Por qué necesitaría un puntero a un puntero?

  • Ten cuidado; la frase “doble puntero” también se refiere al tipo double*.

    –Keith Thompson

    19 de octubre de 2016 a las 2:02

  • Tome nota: la respuesta a esta pregunta es diferente para C y C++; no agregue la etiqueta c+ a esta pregunta muy antigua.

    – BЈовић

    1 de agosto de 2017 a las 8:32

  • @BЈовић Aunque es una pregunta y un comentario antiguos, ¿cuál es la diferencia en el uso del puntero doble entre C y C++? Después de ver su comentario de que son diferentes, traté de dar la respuesta por mí mismo, pero aún veo poca diferencia en el uso de punteros dobles en C y C++.

    – Sangjun Lee

    14 de septiembre de 2020 a las 4:53

  • se puede usar para una matriz irregular de caracteres, es decir, una lista de listas donde cada lista tiene una longitud diferente

    – campo propio

    20 de enero a las 2:40

avatar de usuario
pmg

Si desea tener una lista de caracteres (una palabra), puede usar char *word

Si desea una lista de palabras (una oración), puede usar char **sentence

Si quieres una lista de oraciones (un monólogo), puedes usar char ***monologue

Si quieres una lista de monólogos (una biografía), puedes usar char ****biography

Si desea una lista de biografías (una bio-biblioteca), puede utilizar char *****biolibrary

Si quieres una lista de bio-bibliotecas (un ??lol), puedes usar char ******lol

… …

sí, sé que estas podrían no ser las mejores estructuras de datos


Ejemplo de uso con un muy muy muy aburrido lol

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

int wordsinsentence(char **x) {
    int w = 0;
    while (*x) {
        w += 1;
        x++;
    }
    return w;
}

int wordsinmono(char ***x) {
    int w = 0;
    while (*x) {
        w += wordsinsentence(*x);
        x++;
    }
    return w;
}

int wordsinbio(char ****x) {
    int w = 0;
    while (*x) {
        w += wordsinmono(*x);
        x++;
    }
    return w;
}

int wordsinlib(char *****x) {
    int w = 0;
    while (*x) {
        w += wordsinbio(*x);
        x++;
    }
    return w;
}

int wordsinlol(char ******x) {
    int w = 0;
    while (*x) {
        w += wordsinlib(*x);
        x++;
    }
    return w;
}

int main(void) {
    char *word;
    char **sentence;
    char ***monologue;
    char ****biography;
    char *****biolibrary;
    char ******lol;

    //fill data structure
    word = malloc(4 * sizeof *word); // assume it worked
    strcpy(word, "foo");

    sentence = malloc(4 * sizeof *sentence); // assume it worked
    sentence[0] = word;
    sentence[1] = word;
    sentence[2] = word;
    sentence[3] = NULL;

    monologue = malloc(4 * sizeof *monologue); // assume it worked
    monologue[0] = sentence;
    monologue[1] = sentence;
    monologue[2] = sentence;
    monologue[3] = NULL;

    biography = malloc(4 * sizeof *biography); // assume it worked
    biography[0] = monologue;
    biography[1] = monologue;
    biography[2] = monologue;
    biography[3] = NULL;

    biolibrary = malloc(4 * sizeof *biolibrary); // assume it worked
    biolibrary[0] = biography;
    biolibrary[1] = biography;
    biolibrary[2] = biography;
    biolibrary[3] = NULL;

    lol = malloc(4 * sizeof *lol); // assume it worked
    lol[0] = biolibrary;
    lol[1] = biolibrary;
    lol[2] = biolibrary;
    lol[3] = NULL;

    printf("total words in my lol: %d\n", wordsinlol(lol));

    free(lol);
    free(biolibrary);
    free(biography);
    free(monologue);
    free(sentence);
    free(word);
}

Producción:

total words in my lol: 243

  • Solo quería señalar que un arr[a][b][c] no es un ***arr. Puntero de punteros usan referencias de referencias, mientras que arr[a][b][c] se almacena como una matriz habitual en el orden principal de las filas.

    – MCCCS

    23 de junio de 2018 a las 9:50

  • @pmg puedo usar char *ptr= “mi oración”; y pase ptr a la función ¿por qué ** entonces?

    – naumaan

    26 de marzo de 2021 a las 20:15


  • @user143252 — ¿Qué pasa si tienes "my sentence", "his sentence", "her sentence"y "their sentence"? Tu puedes hacer char *ptr1 = "my sentence"; etc.… pero es más cómodo hacer una matriz de 5 (4 + NULL) punteros: char *ptrs[5] = {"my sentence", "his sentence", ..., NULL}. Cuando pasas esa matriz a una función (foo(ptrs)) la matriz se convierte automáticamente al tipo char**!

    – pmg

    26 de marzo de 2021 a las 20:29


avatar de usuario
Asha

Una razón es que desea cambiar el valor del puntero pasado a una función como argumento de la función, para hacer esto necesita un puntero a un puntero.

En palabras simples, Usar ** cuando desea conservar (O retener el cambio en) la Asignación de memoria o Asignación incluso fuera de una llamada de función. (Entonces, pase dicha función con doble puntero arg.)

Este puede no ser un muy buen ejemplo, pero le mostrará el uso básico:

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

void allocate(int **p)
{
    *p = (int *)malloc(sizeof(int));
}

int main()
{
    int *p = NULL;
    allocate(&p);
    *p = 42;
    printf("%d\n", *p);
    free(p);
}

  • ¿Qué sería diferente si asignar fuera void allocate(int *p) y lo llamaste como allocate(p)?

    – Incerteza

    7 sep 2014 a las 20:03

  • @AlexanderSupertramp Sí. El código fallará. Por favor, vea la respuesta de Silviu.

    – Abhishek

    30 de septiembre de 2014 a las 5:37

  • @Asha, ¿cuál es la diferencia entre allocate(p) y allocate(&p)?

    – usuario2979872

    10 oct 2017 a las 15:23


  • @Asha – ¿No podemos simplemente devolver el puntero? Si debemos mantenerlo vacío, ¿cuál es un caso de uso práctico de este escenario?

    – Shabirmean

    9 oct 2018 a las 23:27

  • @user2979872 allocate(p): p se pasa por valor y, por lo tanto, los cambios en la función no se reflejan en el método principal. allocate(&p): p se pasa por referencia y, por lo tanto, los cambios en p se reflejan en el método principal. Sin embargo, hay una trampa. Si se usa allocate(p) y cambiamos el valor en la dirección señalada por b, entonces los cambios se reflejarán en main() para el valor, ya que el cambio ocurrió en la ubicación de la memoria directamente. Solo para reiterar, el cambio de valor en p aún no se reflejará.

    –Ankit Arora

    9 abr 2021 a las 10:40

avatar de usuario
Brian Joseph Spinos

  • Digamos que tienes un puntero. Su valor es una dirección.
  • pero ahora quieres cambiar esa dirección.
  • tú podrías. haciendo pointer1 = pointer2le das al puntero1 la dirección del puntero2.
  • ¡pero! si hace eso dentro de una función y desea que el resultado persista después de que se complete la función, necesita hacer un trabajo adicional. necesita un nuevo pointer3 solo para apuntar a pointer1. pasar pointer3 a la función.

  • Aquí hay un ejemplo. mire la salida a continuación primero, para entender.

#include <stdio.h>

int main()
{

    int c = 1;
    int d = 2;
    int e = 3;
    int * a = &c;
    int * b = &d;
    int * f = &e;
    int ** pp = &a;  // pointer to pointer 'a'

    printf("\n a's value: %x \n", a);
    printf("\n b's value: %x \n", b);
    printf("\n f's value: %x \n", f);
    printf("\n can we change a?, lets see \n");
    printf("\n a = b \n");
    a = b;
    printf("\n a's value is now: %x, same as 'b'... it seems we can, but can we do it in a function? lets see... \n", a);
    printf("\n cant_change(a, f); \n");
    cant_change(a, f);
    printf("\n a's value is now: %x, Doh! same as 'b'...  that function tricked us. \n", a);

    printf("\n NOW! lets see if a pointer to a pointer solution can help us... remember that 'pp' point to 'a' \n");
     printf("\n change(pp, f); \n");
    change(pp, f);
    printf("\n a's value is now: %x, YEAH! same as 'f'...  that function ROCKS!!!. \n", a);
    return 0;
}

void cant_change(int * x, int * z){
    x = z;
    printf("\n ----> value of 'a' is: %x inside function, same as 'f', BUT will it be the same outside of this function? lets see\n", x);
}

void change(int ** x, int * z){
    *x = z;
    printf("\n ----> value of 'a' is: %x inside function, same as 'f', BUT will it be the same outside of this function? lets see\n", *x);
}

Aquí está la salida: (Leé esto primero)

 a's value: bf94c204

 b's value: bf94c208 

 f's value: bf94c20c 

 can we change a?, lets see 

 a = b 

 a's value is now: bf94c208, same as 'b'... it seems we can, but can we do it in a function? lets see... 

 cant_change(a, f); 

 ----> value of 'a' is: bf94c20c inside function, same as 'f', BUT will it be the same outside of this function? lets see

 a's value is now: bf94c208, Doh! same as 'b'...  that function tricked us. 

 NOW! lets see if a pointer to a pointer solution can help us... remember that 'pp' point to 'a' 

 change(pp, f); 

 ----> value of 'a' is: bf94c20c inside function, same as 'f', BUT will it be the same outside of this function? lets see

 a's value is now: bf94c20c, YEAH! same as 'f'...  that function ROCKS!!!. 

  • Esta es una gran respuesta y realmente me ayudó a visualizar el propósito y la utilidad de un puntero doble.

    – Justin

    11 de abril de 2017 a las 15:11

  • @Justin, ¿revisaste mi respuesta arriba de esta? es mas limpio 🙂

    – Brian Joseph Spinos

    11 de abril de 2017 a las 23:51

  • Gran respuesta, solo falta explicar que void cant_change(int * x, int * z) falla porque sus parámetros son solo nuevos punteros de alcance local que se inicializan de la misma manera que los punteros a y f (por lo que no son lo mismo que a y f).

    – Pedro Reyes

    17 mayo 2017 a las 16:59


  • ¿Sencillo? ¿En serio? 😉

    – alk

    16 de abril de 2019 a las 16:46

  • esta respuesta realmente explica uno de los usos más comunes de puntero a puntero, ¡gracias!

    – tonyjosi

    27 de enero de 2020 a las 5:51

avatar de usuario
Silvio

Agregando a la respuesta de Asha, si usa un solo puntero al ejemplo a continuación (por ejemplo, alloc1() ), perderá la referencia a la memoria asignada dentro de la función.

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

void alloc2(int** p) {
    *p = (int*)malloc(sizeof(int));
    **p = 10;
}

void alloc1(int* p) {
    p = (int*)malloc(sizeof(int));
    *p = 10;
}

int main(){
    int *p = NULL;
    alloc1(p);
    //printf("%d ",*p);//undefined
    alloc2(&p);
    printf("%d ",*p);//will print 10
    free(p);
    return 0;
}

La razón por la que ocurre así es que en alloc1 el puntero se pasa por valor. Entonces, cuando se reasigna al resultado de la malloc llama dentro de alloc1el cambio no pertenece al código en un ámbito diferente.

avatar de usuario
Ziyuán

Vi un muy buen ejemplo hoy, de esta entrada de blogcomo resumo a continuación.

Imagina que tienes una estructura para nodos en una lista enlazada, que probablemente sea

typedef struct node
{
    struct node * next;
    ....
} node;

Ahora desea implementar un remove_if función, que acepta un criterio de eliminación rm como uno de los argumentos y atraviesa la lista enlazada: si una entrada satisface el criterio (algo así como rm(entry)==true), su nodo será eliminado de la lista. Al final, remove_if devuelve el encabezado (que puede ser diferente del encabezado original) de la lista enlazada.

puedes escribir

for (node * prev = NULL, * curr = head; curr != NULL; )
{
    node * const next = curr->next;
    if (rm(curr))
    {
        if (prev)  // the node to be removed is not the head
            prev->next = next;
        else       // remove the head
            head = next;
        free(curr);
    }
    else
        prev = curr;
    curr = next;
}

como tu for círculo. El mensaje es, sin punteros dobles, tienes que mantener un prev variable para reorganizar los punterosy manejar los dos casos diferentes.

Pero con punteros dobles, en realidad puedes escribir

// now head is a double pointer
for (node** curr = head; *curr; )
{
    node * entry = *curr;
    if (rm(entry))
    {
        *curr = entry->next;
        free(entry);
    }
    else
        curr = &entry->next;
}

no necesitas un prev ahora porque puedes modificar directamente lo que prev->next señaló a.

Para aclarar las cosas, sigamos un poco el código. Durante la eliminación:

  1. Si entry == *head: será *head (==*curr) = *head->nexthead ahora apunta al puntero del nuevo nodo de encabezado. Esto se hace cambiando directamente headel contenido de un nuevo puntero.
  2. Si entry != *head: similar, *curr qué es prev->next señaló, y ahora apunta a entry->next.

No importa en qué caso, puede reorganizar los punteros de forma unificada con punteros dobles.

1. Concepto básico –

Cuando declara lo siguiente: –

1. char *ch – (llamado puntero de carácter)

– ch contiene la dirección de un solo carácter.
– (*ch) eliminará la referencia al valor del carácter.

2. char **ch –

‘ch’ contiene la dirección de una matriz de punteros de caracteres. (como en 1)
‘*ch’ contiene la dirección de un solo carácter. (Tenga en cuenta que es diferente de 1, debido a la diferencia en la declaración).
(**ch) eliminará la referencia al valor exacto del carácter.

Agregar más punteros expande la dimensión de un tipo de datos, de carácter a cadena, a matriz de cadenas, etc. Puede relacionarlo con una matriz 1d, 2d, 3d.

Entonces, el uso del puntero depende de cómo lo declare.

Aquí hay un código simple …

int main()
{
    char **p;
    p = (char **)malloc(100);
    p[0] = (char *)"Apple";      // or write *p, points to location of 'A'
    p[1] = (char *)"Banana";     // or write *(p+1), points to location of 'B'

    cout << *p << endl;          //Prints the first pointer location until it finds '\0'
    cout << **p << endl;         //Prints the exact character which is being pointed
    *p++;                        //Increments for the next string
    cout << *p;
}

2. Otra aplicación de punteros dobles –

(esto también cubriría pase por referencia)

Suponga que desea actualizar un carácter de una función. Si intenta lo siguiente: –

void func(char ch)
{
    ch="B";
}

int main()
{
    char ptr;
    ptr="A";
    printf("%c", ptr);

    func(ptr);
    printf("%c\n", ptr);
}

La salida será AA. Esto no funciona, ya que ha “Pasado por valor” a la función.

La forma correcta de hacerlo sería –

void func( char *ptr)        //Passed by Reference
{
    *ptr="B";
}

int main()
{
    char *ptr;
    ptr = (char *)malloc(sizeof(char) * 1);
    *ptr="A";
    printf("%c\n", *ptr);

    func(ptr);
    printf("%c\n", *ptr);
}

Ahora extienda este requisito para actualizar una cadena en lugar de un carácter.
Para esto, debe recibir el parámetro en la función como un puntero doble.

void func(char **str)
{
    strcpy(str, "Second");
}

int main()
{
    char **str;
    // printf("%d\n", sizeof(char));
    *str = (char **)malloc(sizeof(char) * 10);          //Can hold 10 character pointers
    int i = 0;
    for(i=0;i<10;i++)
    {
        str = (char *)malloc(sizeof(char) * 1);         //Each pointer can point to a memory of 1 character.
    }

    strcpy(str, "First");
    printf("%s\n", str);
    func(str);
    printf("%s\n", str);
}

En este ejemplo, el método espera un puntero doble como parámetro para actualizar el valor de una cadena.

avatar de usuario
jason

Los punteros a punteros también son útiles como “identificadores” de la memoria donde desea pasar un “identificador” entre funciones para la memoria reubicable. Básicamente, eso significa que la función puede cambiar la memoria a la que apunta el puntero dentro de la variable handle, y cada función u objeto que usa el handle apuntará correctamente a la memoria recién reubicada (o asignada). A las bibliotecas les gusta hacer esto con tipos de datos “opacos”, es decir, tipos de datos en los que no tiene que preocuparse por lo que están haciendo con la memoria que se apunta, simplemente pasa el “mango” entre el funciones de la biblioteca para realizar algunas operaciones en esa memoria … las funciones de la biblioteca pueden asignar y desasignar la memoria bajo el capó sin que tenga que preocuparse explícitamente por el proceso de administración de la memoria o hacia dónde apunta el controlador.

Por ejemplo:

#include <stdlib.h>

typedef unsigned char** handle_type;

//some data_structure that the library functions would work with
typedef struct 
{
    int data_a;
    int data_b;
    int data_c;
} LIB_OBJECT;

handle_type lib_create_handle()
{
    //initialize the handle with some memory that points to and array of 10 LIB_OBJECTs
    handle_type handle = malloc(sizeof(handle_type));
    *handle = malloc(sizeof(LIB_OBJECT) * 10);

    return handle;
}

void lib_func_a(handle_type handle) { /*does something with array of LIB_OBJECTs*/ }

void lib_func_b(handle_type handle)
{
    //does something that takes input LIB_OBJECTs and makes more of them, so has to
    //reallocate memory for the new objects that will be created

    //first re-allocate the memory somewhere else with more slots, but don't destroy the
    //currently allocated slots
    *handle = realloc(*handle, sizeof(LIB_OBJECT) * 20);

    //...do some operation on the new memory and return
}

void lib_func_c(handle_type handle) { /*does something else to array of LIB_OBJECTs*/ }

void lib_free_handle(handle_type handle) 
{
    free(*handle);
    free(handle); 
}


int main()
{
    //create a "handle" to some memory that the library functions can use
    handle_type my_handle = lib_create_handle();

    //do something with that memory
    lib_func_a(my_handle);

    //do something else with the handle that will make it point somewhere else
    //but that's invisible to us from the standpoint of the calling the function and
    //working with the handle
    lib_func_b(my_handle); 

    //do something with new memory chunk, but you don't have to think about the fact
    //that the memory has moved under the hood ... it's still pointed to by the "handle"
    lib_func_c(my_handle);

    //deallocate the handle
    lib_free_handle(my_handle);

    return 0;
}

Espero que esto ayude,

jason

  • ¿Cuál es la razón por la que el tipo de identificador es char sin firmar**? ¿Funcionaría void** igual de bien?

    –ConnorClark

    31 de mayo de 2016 a las 5:44

  • unsigned char se usa específicamente porque estamos almacenando un puntero a datos binarios que se representarán como bytes sin procesar. Utilizando void requerirá una conversión en algún momento y, por lo general, no es tan legible como la intención de lo que se está haciendo.

    – Jasón

    7 junio 2016 a las 14:15

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad