¿Cómo funcionan los punteros a punteros en C?

171

¿Cómo funcionan los punteros a punteros en C? ¿Cuándo los usarías?

relajarse
fuente
43
No, no la tarea ... solo quería saber ... porque lo veo mucho cuando leo el código C.
1
Un puntero a puntero no es un caso especial de algo, por lo que no entiendo lo que no entiendes sobre vacío **.
akappa
para matrices 2D, el mejor ejemplo es la línea de comando args "prog arg1 arg2" se almacena char ** argv. Y si la persona que llama no quiere asignar la memoria (la función llamada asignará la memoria)
resultados
1
Tienes un buen ejemplo de uso de "puntero a puntero" en Git 2.0: mira mi respuesta a continuación
VonC

Respuestas:

359

Supongamos una computadora de 8 bits con direcciones de 8 bits (y, por lo tanto, solo 256 bytes de memoria). Esto es parte de esa memoria (los números en la parte superior son las direcciones):

  54   55   56   57   58   59   60   61   62   63   64   65   66   67   68   69
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
|    | 58 |    |    | 63 |    | 55 |    |    | h  | e  | l  | l  | o  | \0 |    |
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+

Lo que puedes ver aquí es que en la dirección 63 comienza la cadena "hola". Entonces, en este caso, si esta es la única aparición de "hola" en la memoria, entonces,

const char *c = "hello";

... define ccomo un puntero a la cadena (solo lectura) "hola", y por lo tanto contiene el valor 63. cdebe almacenarse en algún lugar: en el ejemplo anterior en la ubicación 58. Por supuesto, no solo podemos señalar caracteres , pero también a otros punteros. P.ej:

const char **cp = &c;

Ahora cpapunta a c, es decir, contiene la dirección de c(que es 58). Podemos ir aún más lejos. Considerar:

const char ***cpp = &cp;

Ahora cppalmacena la dirección de cp. Por lo tanto, tiene el valor 55 (basado en el ejemplo anterior), y lo adivinó: está almacenado en la dirección 60.


En cuanto a por qué uno usa punteros a punteros:

  • El nombre de una matriz generalmente produce la dirección de su primer elemento. Entonces, si la matriz contiene elementos de tipo t, una referencia a la matriz tiene tipo t *. Ahora considere una matriz de matrices de tipo t: naturalmente, una referencia a esta matriz 2D tendrá type (t *)*= t **, y por lo tanto es un puntero a un puntero.
  • Aunque una serie de cadenas suena unidimensional, de hecho es bidimensional, ya que las cadenas son matrices de caracteres. Por lo tanto: char **.
  • Una función fnecesitará aceptar un argumento de tipo t **para alterar una variable de tipo t *.
  • Muchas otras razones que son demasiado numerosas para enumerarlas aquí.
Stephan202
fuente
77
sí buen ejemplo ... entiendo lo que son ... pero cómo y cuándo usarlos es más importante ... ahora
2
Stephan hizo un buen trabajo reproduciendo, básicamente, el diagrama del lenguaje de programación The C de Kernighan & Richie. Si está programando C, y no tiene este libro y le gusta la documentación en papel, le recomiendo que lo obtenga, el gasto (bastante) modesto se amortizará muy rápidamente en productividad. Tiende a ser muy claro en sus ejemplos.
J. Polfer
44
char * c = "hola" debe ser constante char * c = "hola". Además, es muy engañoso decir que "una matriz se almacena como la dirección del primer elemento". Una matriz se almacena como ... una matriz. A menudo, su nombre arroja un puntero a su primer elemento, pero no siempre. Acerca de los punteros a punteros, simplemente diría que son útiles cuando una función tiene que modificar un puntero pasado como parámetro (en su lugar, pasa un puntero al puntero).
Bastien Léonard
44
A menos que esté malinterpretando esta respuesta, se ve mal. c se almacena en 58 y apunta a 63, cp se almacena en 55 y apunta a 58, y cpp no ​​se representa en el diagrama.
Thanatos
1
Se ve bien. Que un problema menor fue todo lo que me impedía decir: Gran publicación. La explicación en sí fue excelente. Cambio a un voto positivo. (¿Quizás stackoverflow necesita revisar los punteros?)
Thanatos
46

¿Cómo funcionan los punteros a punteros en C?

Primero, un puntero es una variable, como cualquier otra variable, pero que contiene la dirección de una variable.

Un puntero a un puntero es una variable, como cualquier otra variable, pero que contiene la dirección de una variable. Esa variable resulta ser un puntero.

¿Cuándo los usarías?

Puede usarlos cuando necesite devolver un puntero a alguna memoria en el montón, pero no usar el valor de retorno.

Ejemplo:

int getValueOf5(int *p)
{
  *p = 5;
  return 1;//success
}

int get1024HeapMemory(int **p)
{
  *p = malloc(1024);
  if(*p == 0)
    return -1;//error
  else 
    return 0;//success
}

Y lo llamas así:

int x;
getValueOf5(&x);//I want to fill the int varaible, so I pass it's address in
//At this point x holds 5

int *p;    
get1024HeapMemory(&p);//I want to fill the int* variable, so I pass it's address in
//At this point p holds a memory address where 1024 bytes of memory is allocated on the heap

También hay otros usos, como el argumento main () de cada programa en C tiene un puntero a un puntero para argv, donde cada elemento contiene una matriz de caracteres que son las opciones de línea de comando. Sin embargo, debe tener cuidado cuando utilice punteros de punteros para apuntar a matrices bidimensionales, es mejor utilizar un puntero a una matriz bidimensional.

¿Por qué es peligroso?

void test()
{
  double **a;
  int i1 = sizeof(a[0]);//i1 == 4 == sizeof(double*)

  double matrix[ROWS][COLUMNS];
  int i2 = sizeof(matrix[0]);//i2 == 240 == COLUMNS * sizeof(double)
}

Aquí hay un ejemplo de un puntero a una matriz bidimensional hecho correctamente:

int (*myPointerTo2DimArray)[ROWS][COLUMNS]

Sin embargo, no puede usar un puntero a una matriz bidimensional si desea admitir un número variable de elementos para las FILAS y COLUMNAS. Pero cuando sabes de antemano, usarías una matriz bidimensional.

Brian R. Bondy
fuente
32

Me gusta este ejemplo de código de "mundo real" de uso de puntero a puntero, en Git 2.0, confirme 7b1004b :

Linus dijo una vez:

De hecho, desearía que más personas entendieran el tipo de codificación realmente básico de bajo nivel. No son cosas grandes y complejas, como la búsqueda de nombres sin cerradura, sino simplemente un buen uso de punteros a punteros, etc.
Por ejemplo, he visto a demasiadas personas que eliminan una entrada de lista enlazada individualmente haciendo un seguimiento de la entrada "anterior" , y luego para eliminar la entrada, haciendo algo como

if (prev)
  prev->next = entry->next;
else
  list_head = entry->next;

y cada vez que veo un código así, simplemente digo "Esta persona no entiende los punteros". Y lamentablemente es bastante común.

Las personas que entienden los punteros simplemente usan un " puntero al puntero de entrada " e inicializan eso con la dirección de list_head. Y luego, a medida que atraviesan la lista, pueden eliminar la entrada sin usar ningún condicionante, simplemente haciendo un

*pp =  entry->next

http://i.stack.imgur.com/bpfxT.gif

La aplicación de esa simplificación nos permite perder 7 líneas de esta función, incluso al agregar 2 líneas de comentarios.

-   struct combine_diff_path *p, *pprev, *ptmp;
+   struct combine_diff_path *p, **tail = &curr;

Chris señala en los comentarios al video de 2016 " El problema del doble puntero de Linus Torvalds " de Philip Buuck .


Kumar señala en los comentarios la publicación del blog " Linus sobre la comprensión de los punteros ", donde Grisha Trubetskoy explica:

Imagine que tiene una lista vinculada definida como:

typedef struct list_entry {
    int val;
    struct list_entry *next;
} list_entry;

Debe iterar sobre él desde el principio hasta el final y eliminar un elemento específico cuyo valor sea igual al valor de to_remove.
La forma más obvia de hacer esto sería:

list_entry *entry = head; /* assuming head exists and is the first entry of the list */
list_entry *prev = NULL;

while (entry) { /* line 4 */
    if (entry->val == to_remove)     /* this is the one to remove ; line 5 */
        if (prev)
           prev->next = entry->next; /* remove the entry ; line 7 */
        else
            head = entry->next;      /* special case - first entry ; line 9 */

    /* move on to the next entry */
    prev = entry;
    entry = entry->next;
}

Lo que estamos haciendo arriba es:

  • iterar sobre la lista hasta que aparezca la entrada NULL, lo que significa que hemos llegado al final de la lista (línea 4).
  • Cuando nos encontramos con una entrada que queremos eliminar (línea 5),
    • asignamos el valor del siguiente puntero actual al anterior,
    • eliminando así el elemento actual (línea 7).

Hay un caso especial arriba: al comienzo de la iteración no hay una entrada anterior ( preves NULL), por lo que para eliminar la primera entrada de la lista debe modificar el encabezado (línea 9).

Lo que Linus decía es que el código anterior podría simplificarse haciendo que el elemento anterior sea un puntero a un puntero en lugar de solo un puntero .
El código entonces se ve así:

list_entry **pp = &head; /* pointer to a pointer */
list_entry *entry = head;

while (entry) {
    if (entry->val == to_remove)
        *pp = entry->next;

    pp = &entry->next;
    entry = entry->next;
}

El código anterior es muy similar a la variante anterior, pero observe cómo ya no necesitamos estar atentos al caso especial del primer elemento de la lista, ya ppque no está NULLal principio. Simple e inteligente.

Además, alguien en ese hilo comentó que la razón por la que esto es mejor es porque *pp = entry->nextes atómica. Ciertamente NO es atómico .
La expresión anterior contiene dos operadores de desreferencia ( *y ->) y una asignación, y ninguna de esas tres cosas es atómica.
Este es un error común, ¡pero por desgracia casi nada en C debería suponerse que es atómico (incluidos los operadores ++y --)!

VonC
fuente
44
Esto le ayudará a entender mejor - grisha.org/blog/2013/04/02/linus-on-understanding-pointers
Kumar
@kumar buena referencia. Lo he incluido en la respuesta para mayor visibilidad.
VonC
Este video fue esencial para mí para entender tu ejemplo. En particular, me sentía confundido (y beligerante) hasta que dibujé un diagrama de memoria y seguí el progreso del programa. Dicho esto, todavía me parece algo misterioso.
Chris
@Chris Gran video, ¡gracias por mencionarlo! He incluido tu comentario en la respuesta para mayor visibilidad.
VonC
14

Cuando cubrimos consejos en un curso de programación en la universidad, nos dieron dos pistas sobre cómo comenzar a aprender sobre ellos. El primero fue ver Pointer Fun With Binky . El segundo era pensar en los ojos de los eglefinos de A través del espejo de Lewis Carroll

"Estás triste", dijo el Caballero en un tono ansioso: "Déjame cantarte una canción para consolarte".

"¿Es muy largo?" Preguntó Alice, porque había escuchado mucha poesía ese día.

“Es largo”, dijo el Caballero, “pero es muy, muy hermoso. Todos los que me oyen cantarlo ... o les trae lágrimas a los ojos, o bien ...

"¿O si no qué?" dijo Alice, porque el Caballero había hecho una pausa repentina.

“O de lo contrario no, ya sabes. El nombre de la canción se llama 'Haddocks' Eyes '".

"Oh, ese es el nombre de la canción, ¿verdad?", Dijo Alice, tratando de sentirse interesada.

"No, no entiendes", dijo el Caballero, un poco molesto. “Así se llama el nombre. El nombre realmente es 'The Aged Aged Man' ".

"¿Entonces debería haber dicho 'Así se llama la canción'?" Alice se corrigió a sí misma.

“No, no deberías: ¡eso es otra cosa! La canción se llama 'Ways And Means': ¡pero así es como se llama!

"Bueno, ¿cuál es la canción, entonces?" dijo Alice, que para entonces estaba completamente desconcertada.

"Estaba llegando a eso", dijo el Caballero. "La canción realmente es 'A-sitting On A Gate': y la canción es mi propio invento".

Edd
fuente
1
Tuve que leer ese pasaje un par de veces ... ¡+1 por hacerme pensar!
Ruben Steins
Es por eso que Lewis Carroll no es un escritor ordinario.
metarose
1
Entonces ... ¿sería así? nombre -> 'The Aged Aged Man' -> llamado -> 'Haddock's Eyes' -> canción -> 'A-sitting On A Gate'
tisaconundrum
7

Cuando se requiere una referencia a un puntero. Por ejemplo, cuando desea modificar el valor (dirección apuntada) de una variable de puntero declarada en el alcance de una función de llamada dentro de una función llamada.

Si pasa un solo puntero como argumento, modificará copias locales del puntero, no el puntero original en el alcance de la llamada. Con un puntero a un puntero, modifica el último.

Alex Balashov
fuente
Bien explicado para la parte 'Por qué'
Rana Deep
7

Un puntero a un puntero también se llama un identificador . Un uso para esto es a menudo cuando un objeto se puede mover en la memoria o eliminar. A menudo, uno es responsable de bloquear y desbloquear el uso del objeto para que no se mueva al acceder.

A menudo se usa en entornos con memoria restringida, es decir, Palm OS.

enlace computer.howstuffworks.com >>

www.flippinbits.com Enlace >>

epatel
fuente
7

Considere la figura y el programa a continuación para comprender mejor este concepto .

Diagrama de doble puntero

Según la figura, ptr1 es un puntero único que tiene una dirección de variable num .

ptr1 = #

Del mismo modo, ptr2 es un puntero a puntero (puntero doble) que tiene la dirección del puntero ptr1 .

ptr2 = &ptr1;

Un puntero que apunta a otro puntero se conoce como doble puntero. En este ejemplo ptr2 es un puntero doble.

Valores del diagrama anterior:

Address of variable num has : 1000
Address of Pointer ptr1 is: 2000
Address of Pointer ptr2 is: 3000

Ejemplo:

#include <stdio.h>

int main ()
{
   int  num = 10;
   int  *ptr1;
   int  **ptr2;

   // Take the address of var 
   ptr1 = &num;

   // Take the address of ptr1 using address of operator &
   ptr2 = &ptr1;

   // Print the value
   printf("Value of num = %d\n", num );
   printf("Value available at *ptr1 = %d\n", *ptr1 );
   printf("Value available at **ptr2 = %d\n", **ptr2);
}

Salida:

Value of num = 10
Value available at *ptr1 = 10
Value available at **ptr2 = 10
msc
fuente
5

es un puntero al valor de la dirección del puntero. (eso es terrible, lo sé)

básicamente, le permite pasar un puntero al valor de la dirección de otro puntero, por lo que puede modificar dónde apunta otro puntero desde una subfunción, como:

void changeptr(int** pp)
{
  *pp=&someval;
}
Luke Schafer
fuente
lo siento, sé que fue bastante malo. Intenta leer, erm, esto: codeproject.com/KB/cpp/PtrToPtr.aspx
Luke Schafer
5

Tienes una variable que contiene una dirección de algo. Eso es un puntero.

Luego tiene otra variable que contiene la dirección de la primera variable. Eso es un puntero a puntero.

Igor Oks
fuente
3

Un puntero a puntero es, bueno, un puntero a puntero.

Un ejemplo significativo de someType ** es una matriz bidimensional: tiene una matriz, llena de punteros a otras matrices, así que cuando escribe

dpointer [5] [6]

accede a la matriz que contiene punteros a otras matrices en su quinta posición, obtiene el puntero (deje su nombre en fpointer) y luego acceda al sexto elemento de la matriz referenciado a esa matriz (fpointer [6]).

akappa
fuente
2
los punteros a punteros no deben confundirse con las matrices de rango 2, por ejemplo, int x [10] [10] donde escribe x [5] [6] accede al valor en la matriz.
Pete Kirkham
Este es solo un ejemplo donde un vacío ** es apropiado. Un puntero a puntero es solo un puntero que apunta, bueno, a un puntero.
akappa
1

Cómo funciona: es una variable que puede almacenar otro puntero.

¿Cuándo los usaría? Muchos usos uno de ellos es si su función quiere construir una matriz y devolverla a la persona que llama.

//returns the array of roll nos {11, 12} through paramater
// return value is total number of  students
int fun( int **i )
{
    int *j;
    *i = (int*)malloc ( 2*sizeof(int) );
    **i = 11;  // e.g., newly allocated memory 0x2000 store 11
    j = *i;
    j++;
    *j = 12; ;  // e.g., newly allocated memory 0x2004 store 12

    return 2;
}

int main()
{
    int *i;
    int n = fun( &i ); // hey I don't know how many students are in your class please send all of their roll numbers.
    for ( int j=0; j<n; j++ )
        printf( "roll no = %d \n", i[j] );

    return 0;
}
resultados
fuente
0

Hay muchas explicaciones útiles, pero no encontré solo una breve descripción, así que ...

Básicamente, el puntero es la dirección de la variable. Código de resumen corto:

     int a, *p_a;//declaration of normal variable and int pointer variable
     a = 56;     //simply assign value
     p_a = &a;   //save address of "a" to pointer variable
     *p_a = 15;  //override the value of the variable

//print 0xfoo and 15 
//- first is address, 2nd is value stored at this address (that is called dereference)
     printf("pointer p_a is having value %d and targeting at variable value %d", p_a, *p_a); 

También se puede encontrar información útil en el tema Qué significa referencia y desreferencia

Y no estoy tan seguro de cuándo pueden ser útiles los punteros, pero en común es necesario usarlos cuando se realiza una asignación de memoria manual / dinámica: malloc, calloc, etc.

Así que espero que también ayude a aclarar la problemática :)

xxxvodnikxxx
fuente