Aclaración de puntero a puntero

142

Estaba siguiendo este tutorial sobre cómo funciona un puntero a un puntero .

Permítanme citar el pasaje relevante:


    int i = 5, j = 6, k = 7;
    int *ip1 = &i, *ip2 = &j;

Ahora podemos establecer

    int **ipp = &ip1;

y ipppuntos a los ip1que apunta i. *ippes ip1, y **ippes i, o 5. Podemos ilustrar la situación, con nuestra notación familiar de caja y flecha, así:

ingrese la descripción de la imagen aquí

Si entonces decimos

    *ipp = ip2;

hemos cambiado el puntero señalado por ipp(es decir, ip1) para que contenga una copia ip2, de modo que ( ip1) ahora apunte a j:

ingrese la descripción de la imagen aquí


Mi pregunta es: ¿Por qué en la segunda imagen, ipptodavía está apuntando ip1pero no ip2?

Blake
fuente
66
por favor, agregue el tipo de ippcuando lo defina, para que su pregunta esté completa ;-)
zmo
1
Debido a ippque apunta a ip1lo que *ipp = ip2es igual aip1 = ip2
Grijesh Chauhan
1
¿Podemos dejar de escribir asteriscos tan lejos del tipo puntiagudo? int **ippes mucho menos intuitivo que int** ipp, especialmente cuando **ippsignifica algo completamente diferente fuera de las declaraciones de tipo.
Darkhogg
44
@Darkhogg me int **ippparece bastante intuitivo. Significa que estoy haciendo **ippun int. Cual es verdad.
ithisa
2
@ user54609 significados e intuición personal aparte, considere esto: el tipo de ippes int**, así que simplemente escriba en int**lugar de mágico "la impdesreferenciación es una comprensión int" .
Manu343726

Respuestas:

143

Olvídese por un segundo sobre la analogía de señalar. Lo que realmente contiene un puntero es una dirección de memoria. El &operador es la "dirección de", es decir, devuelve la dirección en la memoria de un objeto. El *operador le proporciona el objeto al que se refiere un puntero, es decir, dado un puntero que contiene una dirección, devuelve el objeto en esa dirección de memoria. Entonces, cuando lo hace *ipp = ip2, lo que está haciendo es *ippobtener el objeto en la dirección en la ippque se encuentra ip1y luego asignarlo al ip1valor almacenado ip2, que es la dirección de j.

Simplemente
& -> Dirección de
*-> Valor en

Robert S. Barnes
fuente
14
& y * nunca fueron tan fáciles
Ray
77
Creo que la principal fuente de confusión se debe a la ambigüedad del operador *, que durante la declaración de la variable se usa para indicar que la variable, de hecho, es un puntero a cierto tipo de datos. Pero, por otro lado, también se usa en declaraciones para acceder al contenido de la variable señalada por un puntero (operador de desreferenciación).
Lucas A.
43

Porque cambiaste el valor señalado por ippno el valor de ipp. Entonces, ipptodavía apunta a ip1(el valor de ipp), ip1el valor de ahora es el mismo que ip2el valor de, por lo que ambos apuntan a j.

Esta:

*ipp = ip2;

es lo mismo que:

ip1 = ip2;
Skizz
fuente
11
Puede valer la pena señalar la diferencia entre int *ip1 = &iy *ipp = ip2;, es decir, si elimina el intde la primera instrucción, las asignaciones se ven muy similares, pero *está haciendo algo muy diferente en los dos casos.
Crowman
22

Como la mayoría de las preguntas para principiantes en la etiqueta C, esta pregunta se puede responder volviendo a los primeros principios:

  • Un puntero es un tipo de valor.
  • Una variable contiene un valor.
  • El &operador convierte una variable en un puntero.
  • El *operador convierte un puntero en una variable.

(Técnicamente, debería decir "lvalue" en lugar de "variable", pero creo que es más claro describir las ubicaciones de almacenamiento mutable como "variables").

Entonces tenemos variables:

int i = 5, j = 6;
int *ip1 = &i, *ip2 = &j;

La variable ip1 contiene un puntero. El &operador se convierte ien un puntero y se le asigna ese valor de puntero ip1. Por lo tanto, ip1 contiene un puntero a i.

La variable ip2 contiene un puntero. El &operador se convierte jen un puntero y se le asigna ese puntero ip2. Por lo tanto, ip2 contiene un puntero a j.

int **ipp = &ip1;

La variable ippcontiene un puntero. El &operador convierte la variable ip1en un puntero y se le asigna ese valor de puntero ipp. Por lo tanto, ippcontiene un puntero a ip1.

Resumamos la historia hasta ahora:

  • i contiene 5
  • j contiene 6
  • ip1contiene "puntero a i"
  • ip2contiene "puntero a j"
  • ippcontiene "puntero a ip1"

Ahora decimos

*ipp = ip2;

El *operador vuelve a convertir un puntero en una variable. Buscamos el valor de ipp, que es "puntero a ip1y lo convertimos en una variable. ¿Qué variable? Por ip1supuesto!

Por lo tanto, esta es simplemente otra forma de decir

ip1 = ip2;

Entonces buscamos el valor de ip2. ¿Qué es? "puntero a j". Asignamos ese valor de puntero a ip1, por ip1lo que ahora es "puntero a j"

Solo cambiamos una cosa: el valor de ip1:

  • i contiene 5
  • j contiene 6
  • ip1contiene "puntero a j"
  • ip2contiene "puntero a j"
  • ippcontiene "puntero a ip1"

¿Por qué ipptodavía apunta ip1y no ip2?

Una variable cambia cuando se le asigna. Cuenta las tareas; ¡no puede haber más cambios en las variables que asignaciones! Se empieza por asignar a i, j, ip1, ip2y ipp. Luego asigna a *ipp, que como hemos visto significa lo mismo que "asignar a ip1". Como no lo asignó por ippsegunda vez, ¡no cambió!

Si desea cambiar, ippentonces tendrá que asignar a ipp:

ipp = &ip2;

por ejemplo.

Eric Lippert
fuente
21

Espero que este código pueda ayudar.

#include <iostream>
#include <stdio.h>
using namespace std;

int main()
{
    int i = 5, j = 6, k = 7;
    int *ip1 = &i, *ip2 = &j;
    int** ipp = &ip1;
    printf("address of value i: %p\n", &i);
    printf("address of value j: %p\n", &j);
    printf("value ip1: %p\n", ip1);
    printf("value ip2: %p\n", ip2);
    printf("value ipp: %p\n", ipp);
    printf("address value of ipp: %p\n", *ipp);
    printf("value of address value of ipp: %d\n", **ipp);
    *ipp = ip2;
    printf("value ipp: %p\n", ipp);
    printf("address value of ipp: %p\n", *ipp);
    printf("value of address value of ipp: %d\n", **ipp);
}

produce:

ingrese la descripción de la imagen aquí

michaeltang
fuente
12

Mi opinión muy personal es que las imágenes con flechas apuntando hacia este lado o que hacen que los punteros sean más difíciles de entender. Los hace parecer algunas entidades abstractas y misteriosas. Ellos no son.

Como todo lo demás en su computadora, los punteros son números . El nombre "puntero" es solo una forma elegante de decir "una variable que contiene una dirección".

Por lo tanto, déjenme remover las cosas explicando cómo funciona realmente una computadora.

Tenemos un int, tiene el nombre iy el valor 5. Esto se almacena en la memoria. Como todo lo que está almacenado en la memoria, necesita una dirección, o no podríamos encontrarla. Digamos que itermina en la dirección 0x12345678 y su amigo jcon valor 6 termina justo después. Suponiendo una CPU de 32 bits donde int es de 4 bytes y los punteros son de 4 bytes, las variables se almacenan en la memoria física de esta manera:

Address     Data           Meaning
0x12345678  00 00 00 05    // The variable i
0x1234567C  00 00 00 06    // The variable j

Ahora queremos señalar estas variables. Creamos un puntero a int int* ip1, y one int* ip2. Como todo en la computadora, estas variables de puntero también se asignan en algún lugar de la memoria. Supongamos que terminan en las siguientes direcciones adyacentes en la memoria, inmediatamente después j. Configuramos los punteros para que contengan las direcciones de las variables previamente asignadas: ip1=&i;("copie la dirección de i en ip1") y ip2=&j. Lo que sucede entre líneas es:

Address     Data           Meaning
0x12345680  12 34 56 78    // The variable ip1(equal to address of i)
0x12345684  12 34 56 7C    // The variable ip2(equal to address of j)

Entonces, lo que obtuvimos fueron aún algunos fragmentos de memoria de 4 bytes que contenían números. No hay flechas místicas o mágicas a la vista.

De hecho, con solo mirar un volcado de memoria, no podemos saber si la dirección 0x12345680 contiene un into int*. La diferencia es cómo nuestro programa elige usar los contenidos almacenados en esta dirección. (La tarea de nuestro programa es en realidad decirle a la CPU qué hacer con estos números).

Luego agregamos otro nivel de indirección con int** ipp = &ip1;. De nuevo, solo tenemos un trozo de memoria:

Address     Data           Meaning
0x12345688  12 34 56 80    // The variable ipp

El patrón parece familiar. Otro trozo de 4 bytes que contiene un número.

Ahora, si tuviéramos un volcado de memoria de la pequeña RAM ficticia anterior, podríamos comprobar manualmente dónde apuntan estos punteros. Echamos un vistazo a lo que está almacenado en la dirección de la ippvariable y encontramos los contenidos 0x12345680. Cuál es, por supuesto, la dirección donde ip1se almacena. Podemos ir a esa dirección, verificar el contenido allí y encontrar la dirección de i, y finalmente podemos ir a esa dirección y encontrar el número 5.

Entonces, si tomamos el contenido de ipp, *ippobtendremos la dirección de la variable de puntero ip1. Al escribir *ipp=ip2, copiamos ip2 en ip1, es equivalente a ip1=ip2. En cualquier caso obtendríamos

Address     Data           Meaning
0x12345680  12 34 56 7C    // The variable ip1
0x12345684  12 34 56 7C    // The variable ip2

(Estos ejemplos se dieron para una gran CPU endian)

Lundin
fuente
55
Aunque entiendo su punto de vista, tiene valor pensar en los punteros como entidades abstractas y misteriosas. Cualquier implementación particular de punteros son solo números, pero la estrategia de implementación que bosqueje no es un requisito de implementación, es solo una estrategia común. Los punteros no necesitan ser del mismo tamaño que un int, los punteros no necesitan ser direcciones en un modelo de memoria virtual plana, y así sucesivamente; estos son simplemente detalles de implementación.
Eric Lippert
@EricLippert Creo que uno puede hacer este ejemplo más abstracto al no usar direcciones de memoria o bloques de datos reales. Si se tratara de una tabla que indica algo así como la location, value, variableubicación 1,2,3,4,5y el valor A,1,B,C,3, la idea correspondiente de los punteros podría explicarse fácilmente sin el uso de flechas, que son intrínsecamente confusas. Con cualquier implementación que elija, existe un valor en algún lugar, y esta es la pieza del rompecabezas que se ofusca al modelar con flechas.
MirroredFate
@EricLippert En mi experiencia, la mayoría de los posibles programadores de C que tienen problemas para entender los punteros, son aquellos que fueron alimentados con modelos abstractos y artificiales. La abstracción no es útil, porque todo el propósito del lenguaje C hoy en día es que está cerca del hardware. Si está aprendiendo C pero no tiene la intención de escribir código cerca del hardware, está perdiendo el tiempo . Java, etc. es una opción mucho mejor si no desea saber cómo funcionan las computadoras, sino simplemente hacer una programación de alto nivel.
Lundin
@EricLippert Y sí, pueden existir varias implementaciones oscuras de punteros, donde los punteros no se corresponden necesariamente con las direcciones. Pero dibujar flechas tampoco te ayudará a comprender cómo funcionan. En algún momento, debe abandonar el pensamiento abstracto y bajar al nivel de hardware; de ​​lo contrario, no debería usar C. Hay muchos lenguajes modernos mucho más adecuados para una programación de alto nivel puramente abstracta.
Lundin
@Lundin: Tampoco soy un gran admirador de los diagramas de flechas; La noción de una flecha como datos es complicada. Prefiero pensarlo de manera abstracta pero sin flechas. El &operador en una variable le da una moneda que representa esa variable. El *operador de esa moneda le devuelve la variable. ¡No se requieren flechas!
Eric Lippert
8

Observe las asignaciones:

ipp = &ip1;

resultados ippa señalar ip1.

así que para ippseñalar ip2, debemos cambiar de manera similar,

ipp = &ip2;

que claramente no estamos haciendo. En cambio, estamos cambiando el valor en la dirección señalada por ipp.
Al hacer lo siguiente

*ipp = ip2;

solo estamos reemplazando el valor almacenado en ip1.

ipp = &ip1, Medios *ipp = ip1 = &i,
ahora, *ipp = ip2 = &j.
Entonces, *ipp = ip2es esencialmente igual que ip1 = ip2.

Dipto
fuente
5
ipp = &ip1;

Ninguna asignación posterior ha cambiado el valor de ipp. Es por eso que todavía apunta a ip1.

Lo que haces con *ipp, es decir, con ip1, no cambia el hecho que ippseñala ip1.

Daniel Daranas
fuente
5

Mi pregunta es: ¿Por qué en la segunda imagen, ipp todavía apunta a ip1 pero no a ip2?

pusiste lindas fotos, voy a tratar de hacer un bonito arte ascii:

Como @ Robert-S-Barnes dijo en su respuesta: olvídate de los punteros y de lo que apunta a qué, pero piensa en términos de memoria. Básicamente, int*significa que contiene la dirección de una variable y int**contiene la dirección de una variable que contiene la dirección de una variable. Luego puede usar el álgebra del puntero para acceder a los valores o las direcciones: &foomedias address of fooy *foomedias value of the address contained in foo.

Por lo tanto, como los punteros se trata de lidiar con la memoria, la mejor manera de hacer que eso sea "tangible" es mostrar lo que el álgebra de los punteros le hace a la memoria.

Entonces, aquí está la memoria de su programa (simplificado para el propósito del ejemplo):

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [   |   |   |   |   ]

cuando haces tu código inicial:

int i = 5, j = 6;
int *ip1 = &i, *ip2 = &j;

así es como se ve tu memoria:

name:    i   j ip1 ip2
addr:    0   1   2   3
mem : [  5|  6|  0|  1]

Allí se puede ver ip1y ip2obtiene las direcciones de ie jy ipptodavía no existe. No olvide que las direcciones son simplemente enteros almacenados con un tipo especial.

Luego declara y define ippcomo:

int **ipp = &ip1;

así que aquí está tu memoria:

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [  5|  6|  0|  1|  2]

y luego, está cambiando el valor señalado por la dirección almacenada ipp, que es la dirección almacenada en ip1:

*ipp = ip2;

la memoria del programa es

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [  5|  6|  1|  1|  2]

NB: como int*es un tipo especial, prefiero evitar siempre declarando múltiples punteros en la misma línea, ya que creo que el int *x;o int *x, *y;la notación puede ser engañoso. Prefiero escribirint* x; int* y;

HTH

zmo
fuente
con su ejemplo, el valor inicial de ip2debe ser 3no 4.
Dipto
1
oh, acabo de cambiar la memoria para que coincida con el orden de la declaración. ¿Supongo que arreglé eso al hacerlo?
zmo
5

Porque cuando dices

*ipp = ip2

estás diciendo el "objeto señalado por ipp" para señalar la dirección de la memoria que ip2está apuntando.

No estás diciendo ippque apuntes ip2.

Diego R. Alcantara
fuente
4

Si agrega el operador de desreferencia *al puntero, redirige desde el puntero al objeto señalado.

Ejemplos:

int i = 0;
int *p = &i; // <-- N.B. the pointer declaration also uses the `*`
             //     it's not the dereference operator in this context
*p;          // <-- this expression uses the pointed-to object, that is `i`
p;           // <-- this expression uses the pointer object itself, that is `p`

Por lo tanto:

*ipp = ip2; // <-- you change the pointer `ipp` points to, not `ipp` itself
            //     therefore, `ipp` still points to `ip1` afterwards.
moooeeeep
fuente
3

Si desea ippseñalar ip2, tendría que decir ipp = &ip2;. Sin embargo, esto dejaría ip1todavía apuntando a i.

Andrejovich
fuente
3

Desde el principio te pones

ipp = &ip1;

Ahora desreferenciarlo como,

*ipp = *&ip1 // Here *& becomes 1  
*ipp = ip1   // Hence proved 
Sunil Bojanapally
fuente
3

Considere cada variable representada así:

type  : (name, adress, value)

entonces tus variables deberían ser representadas así

int   : ( i ,  &i , 5 ); ( j ,  &j ,  6); ( k ,  &k , 5 )

int*  : (ip1, &ip1, &i); (ip1, &ip1, &j)

int** : (ipp, &ipp, &ip1)

Como el valor de ippes &ip1así, la inctrucción:

*ipp = ip2;

cambia el valor en la dirección &ip1al valor de ip2, lo que significa que ip1se cambia:

(ip1, &ip1, &i) -> (ip1, &ip1, &j)

Pero ippaun así:

(ipp, &ipp, &ip1)

Entonces, el valor de ippstill, lo &ip1que significa que todavía apunta ip1.

rullof
fuente
1

Porque estás cambiando el puntero de *ipp. Significa

  1. ipp (nombre variable) ---- entra.
  2. adentro ippes la dirección de ip1.
  3. ahora *ippve a (dirección del interior) ip1.

Ahora nos encontramos en ip1. *ipp(es decir ip1) = ip2.
ip2contener la dirección de j.so el ip1contenido será reemplazado por contener de ip2 (es decir, la dirección de j), NO ESTAMOS CAMBIANDO EL ippCONTENIDO. ESO ES.

usuario3286725
fuente
1

*ipp = ip2; implica:

Asignar ip2a la variable apuntada por ipp. Entonces esto es equivalente a:

ip1 = ip2;

Si desea ip2que se guarde la dirección de ipp, simplemente haga lo siguiente:

ipp = &ip2;

Ahora ippapunta a ip2.

Rikayan Bandyopadhyay
fuente
0

ipppuede contener un valor de (es decir, señalar) un puntero al objeto de tipo puntero . Cuando tu lo hagas

ipp = &ip2;  

entonces ippcontiene la dirección de la variable (puntero)ip2 , que es ( &ip2) de tipo puntero a puntero . Ahora la flecha de ippen la segunda foto apuntará ip2.

Wiki dice:
El *operador es un operador de desreferencia que opera en la variable del puntero y devuelve un valor l (variable) equivalente al valor en la dirección del puntero. Esto se llama desreferenciar el puntero.

Aplicando el *operador al ippdesreferenciarlo a un valor l de puntero paraint escribir. El valor l desreferenciado *ippes de tipo puntero aint , puede contener la dirección de un inttipo de datos. Después de la declaración

ipp = &ip1;

ippmantiene la dirección de ip1y *ippmantiene la dirección de (apuntando a) i. Se puede decir que *ippes un alias de ip1. Ambos **ippy *ip1son alias para i.
Haciendo

 *ipp = ip2;  

*ippy ip2ambos apuntan a la misma ubicación pero ipptodavía apuntan a ip1.

Lo *ipp = ip2;que sí sucede es que copia el contenido de ip2(la dirección de j) en ip1(como *ippes un alias para ip1), en efecto haciendo punteros ip1y ip2apuntando al mismo objeto ( j).
Entonces, en la segunda figura, la flecha de ip1y ip2apunta a jmientras ipptodavía apunta a ip1que no se realiza ninguna modificación para cambiar el valor deipp .

hacks
fuente