¿Cuáles son las barreras para comprender los indicadores y qué se puede hacer para superarlas?[cerrado]

StackOverflow https://stackoverflow.com/questions/5727

  •  08-06-2019
  •  | 
  •  

Pregunta

¿Por qué los punteros son un factor de confusión tan importante para muchos estudiantes nuevos, e incluso antiguos, de nivel universitario en C o C++?¿Existe alguna herramienta o proceso de pensamiento que le haya ayudado a comprender cómo funcionan los punteros en el nivel de variable, función y más allá?

¿Cuáles son algunas buenas prácticas que se pueden hacer para llevar a alguien al nivel de "Ah, ja, ya lo tengo", sin que quede estancado en el concepto general?Básicamente, simula escenarios similares.

¿Fue útil?

Solución

Punteros es un concepto que para muchos puede resultar confuso al principio, en particular cuando se trata de copiar valores de puntero y seguir haciendo referencia al mismo bloque de memoria.

Descubrí que la mejor analogía es considerar el puntero como una hoja de papel con la dirección de una casa y el bloque de memoria al que hace referencia como la casa real.De este modo se pueden explicar fácilmente todo tipo de operaciones.

Agregué algo de código Delphi a continuación y algunos comentarios cuando corresponda.Elegí Delphi porque mi otro lenguaje de programación principal, C#, no presenta pérdidas de memoria de la misma manera.

Si sólo desea aprender el concepto de alto nivel de los punteros, debe ignorar las partes denominadas "Diseño de la memoria" en la explicación siguiente.Su objetivo es dar ejemplos de cómo podría verse la memoria después de las operaciones, pero son de naturaleza más baja.Sin embargo, para explicar con precisión cómo funcionan realmente las saturaciones de búfer, era importante agregar estos diagramas.

Descargo de responsabilidad:Para todos los efectos, esta explicación y los diseños de memoria de ejemplo se simplifican enormemente.Hay más gastos generales y muchos más detalles que necesitaría saber si necesita lidiar con la memoria de bajo nivel.Sin embargo, para la intención de explicar la memoria y los punteros, es lo suficientemente preciso.


Supongamos que la clase THouse utilizada a continuación tiene este aspecto:

type
    THouse = class
    private
        FName : array[0..9] of Char;
    public
        constructor Create(name: PChar);
    end;

Cuando inicializa el objeto de la casa, el nombre dado al constructor se copia en el campo privado FName.Hay una razón por la que se define como una matriz de tamaño fijo.

En la memoria, habrá algunos gastos generales asociados con la asignación de la casa. Lo ilustraré a continuación de esta manera:

---[ttttNNNNNNNNNN]---
     ^   ^
     |   |
     |   +- the FName array
     |
     +- overhead

El área "tttt" está sobrecargada; normalmente habrá más para varios tipos de tiempos de ejecución y lenguajes, como 8 o 12 bytes.Es imperativo que cualquier valor almacenado en esta área nunca sea modificado por nada que no sea el asignador de memoria o las rutinas centrales del sistema, o corre el riesgo de bloquear el programa.


Asignar memoria

Consiga que un empresario construya su casa y le dé la dirección de la casa.A diferencia del mundo real, a la asignación de memoria no se le puede decir dónde asignarla, sino que encontrará un lugar adecuado con suficiente espacio e informará la dirección a la memoria asignada.

Es decir, el emprendedor elegirá el lugar.

THouse.Create('My house');

Diseño de memoria:

---[ttttNNNNNNNNNN]---
    1234My house

Mantener una variable con la dirección.

Escriba la dirección de su nueva casa en una hoja de papel.Este documento le servirá como referencia para su casa.Sin este trozo de papel, estás perdido y no puedes encontrar la casa, a menos que ya estés en ella.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...

Diseño de memoria:

    h
    v
---[ttttNNNNNNNNNN]---
    1234My house

Copiar valor del puntero

Simplemente escriba la dirección en una hoja de papel nueva.Ahora tienes dos hojas de papel que te llevarán a la misma casa, no a dos casas separadas.Cualquier intento de seguir la dirección de un documento y reorganizar los muebles de esa casa hará que parezca que la otra casa ha sido modificado de la misma manera, a menos que puedas detectar explícitamente que en realidad es solo una casa.

Nota Este suele ser el concepto que tengo más problemas para explicarle a la gente: dos punteros no significan dos objetos o bloques de memoria.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := h1; // copies the address, not the house
    ...
    h1
    v
---[ttttNNNNNNNNNN]---
    1234My house
    ^
    h2

Liberando la memoria

Derribar la casa.Luego podrás reutilizar el papel para una nueva dirección si así lo deseas, o borrarlo para olvidar la dirección de la casa que ya no existe.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    h := nil;

Aquí primero construyo la casa y consigo su dirección.Luego le hago algo a la casa (úsalo, el...código, lo dejo como ejercicio para el lector), y luego lo libero.Por último borro la dirección de mi variable.

Diseño de memoria:

    h                        <--+
    v                           +- before free
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h (now points nowhere)   <--+
                                +- after free
----------------------          | (note, memory might still
    xx34My house             <--+  contain some data)

Punteros colgantes

Le dices a tu empresario que destruya la casa, pero te olvidas de borrar la dirección de tu papel.Cuando más tarde miras el papel, olvidas que la casa ya no está allí y vas a visitarla, con resultados fallidos (ver también la parte sobre una referencia no válida más abajo).

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    ... // forgot to clear h here
    h.OpenFrontDoor; // will most likely fail

Usando h después de la llamada a .Free podría trabajo, pero eso es pura suerte.Lo más probable es que falle, en el lugar del cliente, en medio de una operación crítica.

    h                        <--+
    v                           +- before free
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h                        <--+
    v                           +- after free
----------------------          |
    xx34My house             <--+

Como puede ver, H todavía apunta a los restos de los datos en la memoria, pero dado que podría no estar completo, usarlo como antes podría fallar.


Pérdida de memoria

Pierdes el papel y no encuentras la casa.Sin embargo, la casa todavía está en algún lugar, y cuando más adelante quieras construir una nueva casa, no podrás reutilizar ese lugar.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    h := THouse.Create('My house'); // uh-oh, what happened to our first house?
    ...
    h.Free;
    h := nil;

Aquí sobrescribimos el contenido del h variable con la dirección de una casa nueva, pero la antigua sigue en pie...en algún lugar.Después de este código, no hay forma de llegar a esa casa y quedará en pie.En otras palabras, la memoria asignada permanecerá asignada hasta que se cierre la aplicación, momento en el que el sistema operativo la eliminará.

Diseño de la memoria después de la primera asignación:

    h
    v
---[ttttNNNNNNNNNN]---
    1234My house

Diseño de la memoria después de la segunda asignación:

                       h
                       v
---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN]
    1234My house       5678My house

Una forma más común de obtener este método es simplemente olvidarse de liberar algo, en lugar de sobrescribirlo como se indicó anteriormente.En términos de Delphi, esto ocurrirá con el siguiente método:

procedure OpenTheFrontDoorOfANewHouse;
var
    h: THouse;
begin
    h := THouse.Create('My house');
    h.OpenFrontDoor;
    // uh-oh, no .Free here, where does the address go?
end;

Después de que se haya ejecutado este método, no hay ningún lugar en nuestras variables donde exista la dirección de la casa, pero la casa todavía está ahí.

Diseño de memoria:

    h                        <--+
    v                           +- before losing pointer
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h (now points nowhere)   <--+
                                +- after losing pointer
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

Como puede ver, los datos antiguos se dejan intactos en la memoria y no serán reutilizados por el asignador de memoria.El asignador realiza un seguimiento de qué áreas de memoria se han utilizado y no las reutilizará a menos que lo libere.


Liberar la memoria pero mantener una referencia (ahora inválida)

Derriba la casa, borra uno de los trozos de papel pero también tienes otro trozo de papel con la dirección antigua, cuando vayas a la dirección no encontrarás una casa, pero puede que encuentres algo que se parezca a las ruinas. de uno.

Tal vez incluso encuentre una casa, pero no es la casa cuya dirección le dieron originalmente y, por lo tanto, cualquier intento de usarla como si le perteneciera podría fracasar terriblemente.

A veces, incluso puede encontrar que una dirección vecina tiene una casa bastante grande que ocupa tres direcciones (calle principal 1-3), y su dirección va al centro de la casa.Cualquier intento de tratar esa parte de la casa grande de 3 direcciones como una sola casa pequeña también podría fracasar terriblemente.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := h1; // copies the address, not the house
    ...
    h1.Free;
    h1 := nil;
    h2.OpenFrontDoor; // uh-oh, what happened to our house?

Aquí la casa fue derribada, según la referencia en h1, y mientras h1 también fue aclarado, h2 todavía tiene la dirección antigua y desactualizada.El acceso a la casa que ya no está en pie puede funcionar o no.

Esta es una variación del puntero colgante de arriba.Vea su diseño de memoria.


Desbordamiento del búfer

Mueves más cosas a la casa de las que caben, y se derraman en la casa o el jardín de los vecinos.Cuando más tarde el dueño de esa casa vecina regrese a casa, encontrará todo tipo de cosas que considerará suyas.

Ésta es la razón por la que elegí una matriz de tamaño fijo.Para preparar el escenario, suponga que la segunda casa que asignamos, por alguna razón, se colocará antes de la primera en la memoria.En otras palabras, la segunda casa tendrá una dirección más baja que la primera.Además, están ubicados uno al lado del otro.

Así, este código:

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := THouse.Create('My other house somewhere');
                         ^-----------------------^
                          longer than 10 characters
                         0123456789 <-- 10 characters

Diseño de la memoria después de la primera asignación:

                        h1
                        v
-----------------------[ttttNNNNNNNNNN]
                        5678My house

Diseño de la memoria después de la segunda asignación:

    h2                  h1
    v                   v
---[ttttNNNNNNNNNN]----[ttttNNNNNNNNNN]
    1234My other house somewhereouse
                        ^---+--^
                            |
                            +- overwritten

La parte que con mayor frecuencia causará un bloqueo es cuando sobrescribe partes importantes de los datos que almacenó que realmente no deben cambiarse al azar.Por ejemplo, podría no ser un problema que se cambiara partes del nombre del H1-House, en términos de bloquear el programa, pero sobrescribir la sobrecarga del objeto probablemente se bloqueará cuando intente usar el objeto roto, como lo hará. sobrescribir enlaces que se almacenan a otros objetos en el objeto.


Listas enlazadas

Cuando sigues una dirección en una hoja de papel, llegas a una casa, y en esa casa hay otra hoja de papel con una nueva dirección, para la siguiente casa de la cadena, y así sucesivamente.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('Home');
    h2 := THouse.Create('Cabin');
    h1.NextHouse := h2;

Aquí creamos un enlace desde nuestra casa de origen a nuestra cabaña.Podemos seguir la cadena hasta que una casa no tenga NextHouse referencia, lo que significa que es la última.Para visitar todas nuestras casas, podríamos utilizar el siguiente código:

var
    h1, h2: THouse;
    h: THouse;
begin
    h1 := THouse.Create('Home');
    h2 := THouse.Create('Cabin');
    h1.NextHouse := h2;
    ...
    h := h1;
    while h <> nil do
    begin
        h.LockAllDoors;
        h.CloseAllWindows;
        h := h.NextHouse;
    end;

Diseño de memoria (agregado NexThouse como un enlace en el objeto, observado con los cuatro llll en el siguiente diagrama):

    h1                      h2
    v                       v
---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL]
    1234Home       +        5678Cabin      +
                   |        ^              |
                   +--------+              * (no link)

En términos básicos, ¿qué es una dirección de memoria?

Una dirección de memoria es, en términos básicos, solo un número.Si piensa en la memoria como una gran variedad de bytes, el primer byte tiene la dirección 0, la siguiente la dirección 1 y así sucesivamente.Esto está simplificado, pero es suficiente.

Entonces este diseño de memoria:

    h1                 h2
    v                  v
---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN]
    1234My house       5678My house

Podría tener estas dos direcciones (la más a la izquierda es la dirección 0):

  • h1 = 4
  • h2 = 23

Lo que significa que nuestra lista enlazada anterior podría verse así:

    h1 (=4)                 h2 (=28)
    v                       v
---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL]
    1234Home      0028      5678Cabin     0000
                   |        ^              |
                   +--------+              * (no link)

Es típico almacenar una dirección que "no apunta a ninguna parte" como una dirección cero.


En términos básicos, ¿qué es un puntero?

Un puntero es simplemente una variable que contiene una dirección de memoria.Por lo general, puede pedirle al lenguaje de programación que le dé su número, pero la mayoría de los lenguajes de programación y los tiempos de lanzamiento intentan ocultar el hecho de que hay un número debajo, solo porque el número en sí no tiene ningún significado para usted.Es mejor pensar en un puntero como una caja negra, es decir.Realmente no sabes ni te preocupas por cómo se implementa realmente, siempre que funcione.

Otros consejos

En mi primera clase de Comp Sci, hicimos el siguiente ejercicio.Por supuesto, esta era una sala de conferencias con aproximadamente 200 estudiantes en ella...

El profesor escribe en la pizarra: int john;

Juan se levanta

El profesor escribe: int *sally = &john;

Sally se levanta y señala a John.

Profesor: int *bill = sally;

Bill se levanta y señala a John.

Profesor: int sam;

Sam se levanta

Profesor: bill = &sam;

Bill ahora señala a Sam.

Creo que entiendes la idea.Creo que pasamos aproximadamente una hora haciendo esto, hasta que repasamos los conceptos básicos de la asignación de punteros.

Una analogía que he encontrado útil para explicar los punteros son los hipervínculos.La mayoría de las personas pueden entender que un enlace en una página web "apunta" a otra página en Internet, y si puede copiar y pegar ese hipervínculo, ambos apuntarán a la misma página web original.Si va y edita esa página original, luego sigue cualquiera de esos enlaces (indicadores) y obtendrá esa nueva página actualizada.

La razón por la que los punteros parecen confundir a tanta gente es que en su mayoría tienen poca o ninguna experiencia en arquitectura informática.Dado que muchos no parecen tener una idea de cómo se implementan realmente las computadoras (la máquina), trabajar en C/C++ parece extraño.

Un ejercicio consiste en pedirles que implementen una máquina virtual simple basada en código de bytes (en cualquier idioma que elijan, Python funciona muy bien para esto) con un conjunto de instrucciones centrado en operaciones de puntero (carga, almacenamiento, direccionamiento directo/indirecto).Luego pídales que escriban programas simples para ese conjunto de instrucciones.

Cualquier cosa que requiera un poco más que una simple suma implicará sugerencias y seguramente lo entenderán.

¿Por qué los punteros son un factor de confusión tan importante para muchos estudiantes nuevos, e incluso antiguos, de nivel universitario en el lenguaje C/C++?

El concepto de marcador de posición para un valor (variables) se relaciona con algo que nos enseñan en la escuela: el álgebra.No existe un paralelo que puedas establecer sin comprender cómo se distribuye físicamente la memoria dentro de una computadora, y nadie piensa en este tipo de cosas hasta que se ocupa de cosas de bajo nivel, en el nivel de comunicaciones C/C++/byte. .

¿Existe alguna herramienta o proceso de pensamiento que le haya ayudado a comprender cómo funcionan los punteros en el nivel de variable, función y más allá?

Casillas de direcciones.Recuerdo que cuando estaba aprendiendo a programar BASIC en microcomputadoras, había estos bonitos libros con juegos y, a veces, había que introducir valores en direcciones particulares.Tenían una imagen de un montón de cajas, etiquetadas incrementalmente con 0, 1, 2...y se explicó que sólo una cosa pequeña (un byte) podía caber en estas cajas, y había muchas: ¡algunas computadoras tenían hasta 65535!Estaban uno al lado del otro y todos tenían una dirección.

¿Cuáles son algunas buenas prácticas que se pueden hacer para llevar a alguien al nivel de "Ah, ja, ya lo tengo", sin que quede estancado en el concepto general?Básicamente, simula escenarios similares.

¿Para un taladro?Haz una estructura:

struct {
char a;
char b;
char c;
char d;
} mystruct;
mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;
cout << 'Start: my_pointer = ' << *my_pointer << endl;
my_pointer++;
cout << 'After: my_pointer = ' << *my_pointer << endl;
my_pointer = &mystruct.a;
cout << 'Then: my_pointer = ' << *my_pointer << endl;
my_pointer = my_pointer + 3;
cout << 'End: my_pointer = ' << *my_pointer << endl;

Mismo ejemplo que el anterior, excepto en C:

// Same example as above, except in C:
struct {
    char a;
    char b;
    char c;
    char d;
} mystruct;

mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;

printf("Start: my_pointer = %c\n", *my_pointer);
my_pointer++;
printf("After: my_pointer = %c\n", *my_pointer);
my_pointer = &mystruct.a;
printf("Then: my_pointer = %c\n", *my_pointer);
my_pointer = my_pointer + 3;
printf("End: my_pointer = %c\n", *my_pointer);

Producción:

Start: my_pointer = s
After: my_pointer = t
Then: my_pointer = r
End: my_pointer = u

¿Quizás eso explique algunos de los conceptos básicos mediante un ejemplo?

La razón por la que al principio me costó entender los punteros es que muchas explicaciones incluyen mucha tontería sobre pasar por referencia.Lo único que esto hace es confundir la cuestión.Cuando usas un parámetro de puntero, estás aún pasando por valor;pero resulta que el valor es una dirección en lugar de, digamos, un int.

Alguien más ya se ha vinculado a este tutorial, pero puedo destacar el momento en el que comencé a comprender los consejos:

Un tutorial sobre punteros y matrices en C:Capítulo 3 - Punteros y cadenas

int puts(const char *s);

Por el momento, ignora la const. El parámetro pasado a puts() es un puntero, ese es el valor de un puntero (ya que todos los parámetros en C se pasan por valor), y el valor de un puntero es la dirección a la que apunta, o, simplemente, una dirección. Así cuando escribimos puts(strA); como hemos visto, estamos pasando la dirección de strA[0].

En el momento en que leí estas palabras, las nubes se abrieron y un rayo de sol me envolvió con una comprensión puntera.

Incluso si eres un desarrollador de VB .NET o C# (como yo) y nunca usas código inseguro, vale la pena entender cómo funcionan los punteros, o no entenderás cómo funcionan las referencias a objetos.Entonces tendrá la noción común pero errónea de que pasar una referencia de objeto a un método copia el objeto.

El "Tutorial sobre punteros y matrices en C" de Ted Jensen me pareció un excelente recurso para aprender sobre punteros.Está dividido en 10 lecciones, comenzando con una explicación de qué son los punteros (y para qué sirven) y terminando con punteros de función. http://home.netcom.com/~tjensen/ptr/cpoint.htm

A partir de ahí, la Guía de programación de redes de Beej enseña la API de sockets Unix, desde la cual puedes comenzar a hacer cosas realmente divertidas. http://beej.us/guide/bgnet/

Las complejidades de los punteros van más allá de lo que podemos enseñar fácilmente.Hacer que los estudiantes se señalen entre sí y usar hojas de papel con las direcciones de las casas son excelentes herramientas de aprendizaje.Hacen un gran trabajo al presentar los conceptos básicos.De hecho, aprender los conceptos básicos es vital para utilizar punteros con éxito.Sin embargo, en el código de producción, es común entrar en escenarios mucho más complejos que los que estas simples demostraciones pueden encapsular.

He estado involucrado con sistemas donde teníamos estructuras que apuntaban a otras estructuras que apuntaban a otras estructuras.Algunas de esas estructuras también contenían estructuras incrustadas (en lugar de punteros a estructuras adicionales).Aquí es donde los consejos se vuelven realmente confusos.Si tienes múltiples niveles de direccionamiento indirecto y comienzas a terminar con un código como este:

widget->wazzle.fizzle = fazzle.foozle->wazzle;

puede volverse confuso muy rápidamente (imagine muchas más líneas y potencialmente más niveles).Agregue matrices de punteros y punteros de nodo a nodo (árboles, listas vinculadas) y la cosa empeorará aún más.He visto a algunos desarrolladores realmente buenos perderse una vez que comenzaron a trabajar en dichos sistemas, incluso desarrolladores que entendían muy bien los conceptos básicos.

Las estructuras complejas de punteros tampoco indican necesariamente una codificación deficiente (aunque pueden hacerlo).La composición es una pieza vital de una buena programación orientada a objetos y, en lenguajes con punteros sin formato, conducirá inevitablemente a una dirección indirecta de múltiples capas.Además, los sistemas a menudo necesitan utilizar bibliotecas de terceros con estructuras que no coinciden entre sí en estilo o técnica.En situaciones como ésta, naturalmente surgirá complejidad (aunque, ciertamente, deberíamos luchar contra ella tanto como sea posible).

Creo que lo mejor que pueden hacer las universidades para ayudar a los estudiantes a aprender punteros es utilizar buenas demostraciones, combinadas con proyectos que requieran el uso de punteros.Un proyecto difícil hará más por la comprensión de los punteros que mil demostraciones.Las demostraciones pueden brindarle una comprensión superficial, pero para comprender profundamente los consejos, debe usarlos realmente.

Pensé en agregar una analogía a esta lista que encontré muy útil al explicar consejos (en el pasado) como tutor de informática;primero, hagamos:


Preparar el escenario:

Considere un estacionamiento con 3 espacios, estos espacios están numerados:

-------------------
|     |     |     |
|  1  |  2  |  3  |
|     |     |     |

En cierto modo, esto es como ubicaciones de memoria, son secuenciales y contiguas.algo así como una matriz.En este momento no hay autos en ellos, por lo que es como una matriz vacía (parking_lot[3] = {0}).


Añade los datos

Un aparcamiento nunca permanece vacío mucho tiempo...si lo hiciera, sería inútil y nadie construiría ninguno.Entonces, digamos que a medida que avanza el día, el estacionamiento se llena con 3 autos, un auto azul, un auto rojo y un auto verde:

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |R| | |G| |
| o-o | o-o | o-o |

Estos autos son todos del mismo tipo (automóvil), por lo que una forma de pensar en esto es que nuestros autos son algún tipo de datos (por ejemplo, un int) pero tienen valores diferentes (blue, red, green;ese podría ser un color enum)


Introduzca el puntero

Ahora, si te llevo a este estacionamiento y te pido que me busques un auto azul, extiendes un dedo y lo usas para señalar un auto azul en el lugar 1.Esto es como tomar un puntero y asignarlo a una dirección de memoria (int *finger = parking_lot)

Tu dedo (el índice) no es la respuesta a mi pregunta.Mirando en tu dedo no me dice nada, pero si miro donde está tu dedo apuntando a (eliminando la referencia del puntero), puedo encontrar el auto (los datos) que estaba buscando.


Reasignar el puntero

Ahora puedo pedirte que busques un auto rojo y puedes redirigir tu dedo a un auto nuevo.Ahora tu puntero (el mismo de antes) me muestra nuevos datos (el lugar de estacionamiento donde se encuentra el auto rojo) del mismo tipo (el auto).

El puntero no ha cambiado físicamente, todavía está su dedo, solo cambiaron los datos que me estaba mostrando.(la dirección del "lugar de estacionamiento")


Punteros dobles (o un puntero a un puntero)

Esto también funciona con más de un puntero.Puedo preguntar dónde está el puntero, que apunta al auto rojo y puedes usar la otra mano y señalar con el dedo índice el dedo índice.(esto es como int **finger_two = &finger)

Ahora, si quiero saber dónde está el auto azul, puedo seguir la dirección del primer dedo hasta el segundo dedo, hasta el auto (los datos).


El puntero colgante

Ahora digamos que te sientes como una estatua y quieres mantener tu mano apuntando al auto rojo indefinidamente.¿Qué pasa si ese auto rojo se aleja?

   1     2     3
-------------------
| o=o |     | o=o |
| |B| |     | |G| |
| o-o |     | o-o |

Tu puntero sigue apuntando hacia donde está el coche rojo. era pero ya no lo es.Digamos que un auto nuevo se detiene allí...un auto naranja.Ahora, si te vuelvo a preguntar "¿dónde está el auto rojo?", todavía estás señalando allí, pero ahora estás equivocado.Ese no es un auto rojo, es naranja.


aritmética de punteros

Ok, todavía estás señalando el segundo lugar de estacionamiento (ahora ocupado por el auto naranja).

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |O| | |G| |
| o-o | o-o | o-o |

Bueno ahora tengo una nueva pregunta...quiero saber el color del auto en el próximo Estacionamiento.Puedes ver que estás apuntando al punto 2, así que simplemente sumas 1 y apuntas al siguiente punto.(finger+1), ahora como quería saber cuáles eran los datos allí, debes verificar ese lugar (no solo el dedo) para poder deferenciar el puntero (*(finger+1)) para ver que hay un auto verde presente allí (los datos en esa ubicación)

No creo que los punteros como concepto sean particularmente complicados: los modelos mentales de la mayoría de los estudiantes se corresponden con algo como esto y algunos bocetos rápidos de cuadros pueden ayudar.

La dificultad, al menos la que he experimentado en el pasado y he visto a otros enfrentar, es que la gestión de punteros en C/C++ puede ser innecesariamente complicada.

Un ejemplo de tutorial con un buen conjunto de diagramas ayuda enormemente a comprender los punteros..

Joel Spolsky hace algunos buenos comentarios sobre la comprensión de los consejos en su Guía guerrillera para entrevistas artículo:

Por alguna razón, la mayoría de las personas parecen nacer sin la parte del cerebro que entiende los indicadores.Esto es una cuestión de aptitud, no de habilidad; requiere una forma compleja de pensamiento doblemente dirigido que algunas personas simplemente no pueden hacer.

Creo que la principal barrera para comprender los consejos son los malos profesores.

A casi todo el mundo se le enseñan mentiras sobre los consejos:Que ellos son nada más que direcciones de memoria, o que te permitan señalar ubicaciones arbitrarias.

Y por supuesto que son difíciles de entender, peligrosos y semimágicos.

Nada de lo cual es cierto.Los indicadores son en realidad conceptos bastante simples, siempre y cuando te ciñas a lo que el lenguaje C++ tiene que decir sobre ellos y no les imbuya de atributos que "generalmente" funcionan en la práctica, pero que, sin embargo, no están garantizados por el lenguaje y, por lo tanto, no son parte del concepto real de un puntero.

Intenté escribir una explicación de esto hace unos meses en esta publicación de blog - Con suerte, ayudará a alguien.

(Tenga en cuenta que antes de que alguien se ponga pedante conmigo, sí, el estándar C++ dice que los punteros representar direcciones de memoria.Pero no dice que "los punteros son direcciones de memoria, y nada más que direcciones de memoria y pueden usarse o pensarse indistintamente con direcciones de memoria".La distinción es importante)

El problema con los punteros no es el concepto.Es la ejecución y el lenguaje involucrados.Se produce una confusión adicional cuando los profesores asumen que lo difícil es el CONCEPTO de los punteros, y no la jerga o el complicado lío que C y C++ hacen con el concepto.Se desperdicia una gran cantidad de esfuerzo para explicar el concepto (como en la respuesta aceptada para esta pregunta) y prácticamente se desperdicia en alguien como yo, porque ya entiendo todo eso.Simplemente está explicando la parte equivocada del problema.

Para darle una idea de dónde vengo, soy alguien que entiende perfectamente los punteros y puedo usarlos de manera competente en lenguaje ensamblador.Porque en lenguaje ensamblador no se les llama punteros.Se les conoce como direcciones.Cuando se trata de programación y uso de punteros en C, cometo muchos errores y me confundo mucho.Todavía no he solucionado esto.Dejame darte un ejemplo.

Cuando una API dice:

int doIt(char *buffer )
//*buffer is a pointer to the buffer

¿Qué quiere?

podría querer:

un número que representa una dirección a un buffer

(Para darle eso, ¿digo? doIt(mybuffer), o doIt(*myBuffer)?)

un número que representa la dirección a una dirección a un búfer

(es eso doIt(&mybuffer) o doIt(mybuffer) o doIt(*mybuffer)?)

un número que representa la dirección a la dirección a la dirección al búfer

(tal vez eso es doIt(&mybuffer).O es eso doIt(&&mybuffer) ?o incluso doIt(&&&mybuffer))

y así sucesivamente, y el lenguaje involucrado no lo deja tan claro porque involucra las palabras "puntero" y "referencia" que no tienen tanto significado y claridad para mí como "x tiene la dirección de y" y " esta función requiere una dirección para y".La respuesta también depende de qué diablos es "mybuffer" para empezar y qué pretende hacer con él.El lenguaje no soporta los niveles de anidamiento que se encuentran en la práctica.Como cuando tengo que entregar un "puntero" a una función que crea un nuevo búfer y modifica el puntero para que apunte a la nueva ubicación del búfer.¿Realmente quiere el puntero, o un puntero al puntero, para saber dónde ir para modificar el contenido del puntero?La mayor parte del tiempo sólo tengo que adivinar lo que se entiende por "puntero" y la mayoría de las veces me equivoco, independientemente de la experiencia que tenga en adivinar.

"Pointer" está demasiado sobrecargado.¿Es un puntero una dirección a un valor?o es una variable que contiene una dirección a un valor.Cuando una función quiere un puntero, ¿quiere la dirección que contiene la variable del puntero o quiere la dirección de la variable del puntero?Estoy confundido.

Creo que lo que hace que los punteros sean difíciles de aprender es que hasta que los punteros se sientan cómodos con la idea de que "en esta ubicación de memoria hay un conjunto de bits que representan un int, un doble, un carácter, lo que sea".

Cuando ves un puntero por primera vez, realmente no entiendes qué hay en esa ubicación de memoria."¿Qué quieres decir con que tiene una DIRECCIÓN?"

No estoy de acuerdo con la idea de que "o los obtienes o no".

Se vuelven más fáciles de entender cuando comienzas a encontrarles usos reales (como no pasar estructuras grandes a funciones).

La razón por la que es tan difícil de entender no es porque sea un concepto difícil sino porque la sintaxis es inconsistente.

   int *mypointer;

Primero aprende que la parte más a la izquierda de la creación de una variable define el tipo de variable.La declaración de puntero no funciona así en C y C++.En cambio, dicen que la variable apunta al tipo de la izquierda.En este caso: *mi puntero esta apuntando en un int.

No entendí completamente los punteros hasta que intenté usarlos en C# (sin seguridad), funcionan exactamente de la misma manera pero con una sintaxis lógica y consistente.El puntero es un tipo en sí mismo.Aquí mi puntero es un puntero a un int.

  int* mypointer;

Ni siquiera me hagas hablar de los punteros de función...

Podía trabajar con punteros cuando solo conocía C++.Sabía qué hacer en algunos casos y qué no hacer por prueba/error.Pero lo que me dio una comprensión completa es el lenguaje ensamblador.Si realiza una depuración seria a nivel de instrucción con un programa en lenguaje ensamblador que haya escrito, debería poder comprender muchas cosas.

Me gusta la analogía con la dirección de la casa, pero siempre he pensado que la dirección es la del buzón.De esta forma puede visualizar el concepto de desreferenciar el puntero (abrir el buzón).

Por ejemplo, siguiendo una lista vinculada:1) Comience con su documento con la dirección 2) Vaya a la dirección en el documento 3) Abra el buzón para encontrar un nuevo papel con la siguiente dirección.

En una lista enlazada lineal, el último buzón no tiene nada (final de la lista).En una lista circular enlazada, el último buzón tiene la dirección del primer buzón.

Tenga en cuenta que el paso 3 es donde se produce la desreferencia y donde fallará o se equivocará cuando la dirección no sea válida.Suponiendo que puedas caminar hasta el buzón de una dirección no válida, imagina que hay un agujero negro o algo allí que pone el mundo al revés :)

Creo que la razón principal por la que la gente tiene problemas con esto es porque generalmente no se enseña de una manera interesante y atractiva.Me gustaría ver a un conferenciante tomar 10 voluntarios de la multitud y darles una regla de 1 metro a cada uno, hacer que se paren en una configuración determinada y usar las reglas para señalarse entre sí.Luego, muestre la aritmética de punteros moviendo a las personas (y hacia dónde apuntan sus reglas).Sería una forma sencilla pero efectiva (y sobre todo memorable) de mostrar los conceptos sin empantanarse demasiado en la mecánica.

Una vez que llegas a C y C++, parece volverse más difícil para algunas personas.No estoy seguro de si esto se debe a que finalmente están poniendo en práctica una teoría que no comprenden adecuadamente o porque la manipulación del puntero es inherentemente más difícil en esos lenguajes.No recuerdo muy bien mi propia transición, pero sabía punteros en Pascal y luego se trasladó a C y se perdió por completo.

No creo que los indicadores en sí sean confusos.La mayoría de la gente puede entender el concepto.Ahora, ¿en cuántos consejos puedes pensar o con cuántos niveles de direccionamiento indirecto te sientes cómodo?No se necesitan demasiados para poner a la gente al límite.El hecho de que puedan cambiarse accidentalmente por errores en su programa también puede hacer que sea muy difícil depurarlos cuando algo sale mal en su código.

Creo que en realidad podría ser un problema de sintaxis.La sintaxis de C/C++ para punteros parece inconsistente y más compleja de lo necesario.

Irónicamente, lo que realmente me ayudó a comprender los punteros fue encontrar el concepto de iterador en C++. Biblioteca de plantillas estándar.Es irónico porque sólo puedo suponer que los iteradores fueron concebidos como una generalización del puntero.

A veces simplemente no puedes ver el bosque hasta que aprendes a ignorar los árboles.

La confusión proviene de las múltiples capas de abstracción mezcladas en el concepto de "puntero".Los programadores no se confunden con las referencias ordinarias en Java/Python, pero los punteros son diferentes porque exponen características de la arquitectura de memoria subyacente.

Es un buen principio separar claramente las capas de abstracción, y los punteros no hacen eso.

La forma en que me gustaba explicarlo era en términos de matrices e índices: es posible que las personas no estén familiarizadas con los punteros, pero generalmente saben qué es un índice.

Entonces digo, imagina que la RAM es una matriz (y solo tienes 10 bytes de RAM):

unsigned char RAM[10] = { 10, 14, 4, 3, 2, 1, 20, 19, 50, 9 };

Entonces, un puntero a una variable es en realidad solo el índice (el primer byte) de esa variable en la RAM.

Entonces, si tienes un puntero/índice unsigned char index = 2, entonces el valor es obviamente el tercer elemento, o el número 4.Un puntero a un puntero es donde tomas ese número y lo usas como un índice en sí mismo, como RAM[RAM[index]].

Dibujaría una matriz en una lista de papel y simplemente la usaría para mostrar cosas como muchos punteros que apuntan a la misma memoria, aritmética de punteros, puntero a puntero, etc.

Número de apartado postal.

Es un dato que te permite acceder a algo más.

(Y si haces aritmética con números de apartados postales, puedes tener un problema, porque la carta va en el buzón equivocado.Y si alguien se muda a otro estado, sin dirección de reenvío, entonces tiene un indicador pendiente.Por otro lado, si la oficina de correos reenvía el correo, entonces tendrá un puntero a otro).

No es una mala manera de entenderlo, a través de iteradores.pero sigue mirando y verás que Alexandrescu empieza a quejarse de ellos.

Muchos ex desarrolladores de C++ (que nunca entendieron que los iteradores son un puntero moderno antes de deshacerse del lenguaje) saltan a C# y todavía creen que tienen iteradores decentes.

Hmm, el problema es que todos los iteradores están en total desacuerdo con lo que las plataformas de tiempo de ejecución (Java/CLR) están tratando de lograr:Uso nuevo, sencillo y en el que todos son desarrolladores.Lo cual puede ser bueno, pero lo dijeron una vez en el libro morado y lo dijeron incluso antes y antes C:

Indirección.

Un concepto muy poderoso, pero nunca lo será si lo haces hasta el final.Los iteradores son útiles porque ayudan con la abstracción de algoritmos, otro ejemplo.Y el tiempo de compilación es el lugar para un algoritmo, muy simple.Ya sabes código + datos, o en ese otro lenguaje C#:

IEnumerable + LINQ + Massive Framework = penalización de tiempo de ejecución de 300 MB por direccionamiento indirecto de aplicaciones pésimas y que arrastran a través de montones de instancias de tipos de referencia.

"Le Pointer es barato."

Algunas respuestas anteriores han afirmado que "los punteros no son realmente difíciles", pero no se han dirigido directamente dónde "¡El puntero es difícil!" viene de.Hace algunos años di clases particulares a estudiantes de primer año de informática (solo durante un año, ya que claramente no lo hacía) y tenía claro que el idea del puntero no es difícil.lo que es dificil es entender por qué y cuándo querrías un puntero.

No creo que se pueda separar esa pregunta (por qué y cuándo usar un puntero) de la explicación de cuestiones más amplias de ingeniería de software.Por qué cada variable debería no ser una variable global, y por qué uno debería factorizar código similar en funciones (eso, entiende esto, usa punteros para especializar su comportamiento en su sitio de llamada).

No veo qué es tan confuso acerca de los punteros.Apuntan a una ubicación en la memoria, es decir, almacena la dirección de la memoria.En C/C++ puede especificar el tipo al que apunta el puntero.Por ejemplo:

int* my_int_pointer;

Dice que my_int_pointer contiene la dirección de una ubicación que contiene un int.

El problema con los punteros es que apuntan a una ubicación en la memoria, por lo que es fácil desviarse hacia alguna ubicación en la que no debería estar.Como prueba, observe los numerosos agujeros de seguridad en las aplicaciones C/C++ debido al desbordamiento del búfer (incrementando el puntero más allá del límite asignado).

Sólo para confundir un poco más las cosas, a veces hay que trabajar con identificadores en lugar de punteros.Los identificadores son punteros a punteros, de modo que el back-end puede mover cosas en la memoria para desfragmentar el montón.Si el puntero cambia a mitad de la rutina, los resultados son impredecibles, por lo que primero debes bloquear el mango para asegurarte de que nada vaya a ninguna parte.

http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5 Habla de ello con un poco más de coherencia que yo.:-)

Todo principiante de C/C++ tiene el mismo problema y ese problema ocurre no porque "los punteros sean difíciles de aprender" sino "quién y cómo se explican".Algunos alumnos lo recogen verbalmente y otros visualmente y la mejor forma de explicarlo es utilizar ejemplo de "tren" (trajes para ejemplo verbal y visual).

Dónde "locomotora" es un indicador que no puedo sostener cualquier cosa y "vagón" es lo que la "locomotora" intenta tirar (o señalar).Después, puedes clasificar el "carro" en sí, si puede contener animales, plantas o personas (o una combinación de ellos).

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top