¿Cuáles son las barreras para entender los punteros y qué se puede hacer para superarlos? [cerrado]

449

¿Por qué los indicadores son un factor de confusión tan importante para muchos estudiantes nuevos, e incluso antiguos, de nivel universitario en C o C ++? ¿Existen herramientas o procesos de pensamiento que lo ayudaron a comprender cómo funcionan los punteros a nivel variable, funcional y más allá?

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

David McGraw
fuente
20
La tesis de esta pregunta es que los punteros son difíciles de entender. La pregunta no ofrece evidencia de que los punteros sean más difíciles de entender que cualquier otra cosa.
bmargulies
14
Tal vez me falta algo (porque codifico en los idiomas de GCC), pero siempre pensé si los punteros en la memoria son una estructura Clave-> Valor. Dado que es costoso transferir grandes cantidades de datos en un programa, crea la estructura (valor) y pasa su puntero / referencia (clave) porque la clave es una representación mucho más pequeña de la estructura más grande. La parte difícil es cuando necesita comparar dos punteros / referencias (está comparando las claves o los valores) que requiere más trabajo para dividir los datos contenidos dentro de la estructura (valor).
Evan Plaice
2
@ Wolfpack'08 "Me parece que una memoria en la dirección siempre será un int". - Entonces debería parecerte que nada tiene un tipo, ya que todos son solo bits en la memoria. "En realidad, el tipo de puntero es el tipo de var al que apunta el puntero" - No, el tipo de puntero es puntero al tipo de var al que apunta el puntero - lo cual es natural y debería ser obvio.
Jim Balter
2
Siempre me pregunté qué es tan difícil de entender en el hecho de que las variables (y funciones) son solo bloques de memoria y los punteros son variables que almacenan direcciones de memoria. Este modelo de pensamiento quizás demasiado práctico podría no impresionar a todos los fanáticos de los conceptos abstractos, pero ayuda perfectamente a comprender cómo funcionan los punteros.
Christian Rau
8
En pocas palabras, los estudiantes probablemente no entienden porque no entienden correctamente, o en absoluto, cómo la memoria de un ordenador en general, y específicamente los C "modelo" de memoria obras. Este libro Programación desde cero ofrece una muy buena lección sobre estos temas.
Abbafei

Respuestas:

745

Los punteros son un concepto que para muchos puede ser confuso al principio, en particular cuando se trata de copiar valores de puntero y aún hacer referencia al mismo bloque de memoria.

He descubierto que la mejor analogía es considerar el puntero como un trozo de papel con una dirección de la casa y el bloque de memoria al que hace referencia como la casa real. Todo tipo de operaciones se pueden explicar fácilmente.

He agregado un código de Delphi a continuación, y algunos comentarios cuando corresponde. Elegí Delphi porque mi otro lenguaje de programación principal, C #, no exhibe cosas como pérdidas de memoria de la misma manera.

Si solo desea aprender el concepto de alto nivel de punteros, debe ignorar las partes etiquetadas como "Diseño de memoria" en la explicación a continuación. Su objetivo es dar ejemplos de cómo se vería la memoria después de las operaciones, pero son de naturaleza de nivel más bajo. Sin embargo, para explicar con precisión cómo funcionan realmente los desbordamientos del búfer, fue importante que agregue 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 a bajo nivel. Sin embargo, para los intentos de explicar la memoria y los punteros, es lo suficientemente preciso.


Supongamos que la clase THouse utilizada a continuación se ve así:

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, ilustraré esto a continuación de esta manera:

--- [ttttNNNNNNNNNN] ---
     ^ ^
     El | El |
     El | + - la matriz FName
     El |
     + - gastos generales

El área "tttt" está sobrecargada, normalmente habrá más de esto para varios tipos de tiempos de ejecución e idiomas, como 8 o 12 bytes. Es imperativo que cualquier valor que se almacene en esta área nunca se modifique por otra cosa que no sea el asignador de memoria o las rutinas centrales del sistema, o corre el riesgo de bloquear el programa.


Asignar memoria

Haz que un emprendedor construya tu casa y te dé la dirección de la casa. A diferencia del mundo real, no se puede decir a la asignación de memoria dónde asignar, pero encontrará un lugar adecuado con suficiente espacio e informará la dirección a la memoria asignada.

En otras palabras, el empresario elegirá el lugar.

THouse.Create('My house');

Diseño de memoria:

--- [ttttNNNNNNNNNN] ---
    1234Mi casa

Mantenga una variable con la dirección

Escriba la dirección de su nueva casa en una hoja de papel. Este documento servirá como referencia para su casa. Sin este pedazo 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] ---
    1234Mi casa

Copiar valor del puntero

Simplemente escriba la dirección en una nueva hoja de papel. Ahora tiene dos hojas de papel que lo 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 en esa casa hará que parezca que la otra casa ha sido modificada de la misma manera, a menos que pueda detectar explícitamente que en realidad es solo una casa.

Nota: Este es generalmente el concepto que tengo más problemas para explicar a las personas, dos punteros no significa 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] ---
    1234Mi casa
    ^
    h2

Liberando la memoria

Demoler la casa. Luego, puede reutilizar el papel para una nueva dirección si así lo desea, 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 obtengo su dirección. Luego hago algo a la casa (lo uso, el ... código, lo dejé como ejercicio para el lector), y luego lo libero. Por último, borro la dirección de mi variable.

Diseño de memoria:

    h <- +
    v + - antes gratis
--- [ttttNNNNNNNNNN] --- |
    1234Mi casa <- +

    h (ahora apunta a ninguna parte) <- +
                                + - después de gratis
---------------------- | (nota, la memoria aún podría
    xx34 Mi casa <- + contiene algunos datos)

Punteros colgantes

Le dice a su empresario que destruya la casa, pero se olvida de borrar la dirección de su hoja de papel. Cuando más tarde mira el pedazo de papel, ha olvidado que la casa ya no está allí y va a visitarlo, con resultados fallidos (vea también la parte sobre una referencia no válida a continuación).

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

Usar hdespués de la llamada a .Free podría funcionar, pero eso es pura suerte. Lo más probable es que falle, en el lugar de un cliente, en medio de una operación crítica.

    h <- +
    v + - antes gratis
--- [ttttNNNNNNNNNN] --- |
    1234Mi casa <- +

    h <- +
    v + - después de liberar
---------------------- |
    xx34Mi casa <- +

Como puede ver, h todavía apunta a los remanentes de los datos en la memoria, pero dado que puede no estar completo, su uso como antes podría fallar.


Pérdida de memoria

Pierdes el papel y no puedes encontrar la casa. Sin embargo, la casa todavía está parada en algún lugar, y cuando más tarde quieras construir una nueva casa, no puedes 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 de la hvariable con la dirección de una casa nueva, pero la antigua todavía está en pie ... en algún lugar. Después de este código, no hay forma de llegar a esa casa, y se dejará en pie. En otras palabras, la memoria asignada permanecerá asignada hasta que se cierre la aplicación, momento en el cual el sistema operativo la destruirá.

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

    h
    v
--- [ttttNNNNNNNNNN] ---
    1234Mi casa

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

                       h
                       v
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
    1234Mi casa 5678Mi casa

Una forma más común de obtener este método es 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 este método se haya ejecutado, no hay lugar en nuestras variables donde exista la dirección de la casa, pero la casa todavía está ahí afuera.

Diseño de memoria:

    h <- +
    v + - antes de perder el puntero
--- [ttttNNNNNNNNNN] --- |
    1234Mi casa <- +

    h (ahora apunta a ninguna parte) <- +
                                + - después de perder el puntero
--- [ttttNNNNNNNNNN] --- |
    1234Mi casa <- +

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


Liberando la memoria pero manteniendo una referencia (ahora no válida)

Demoler la casa, borrar una de las hojas de papel, pero también tiene otra hoja de papel con la dirección anterior, cuando vaya a la dirección, no encontrará una casa, pero puede encontrar algo que se parezca a las ruinas de uno.

Tal vez incluso encuentre una casa, pero no es la casa a la que se le dio la dirección originalmente, y por lo tanto, cualquier intento de usarla como si le perteneciera podría fallar horriblemente.

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

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 demolida, a través de la referencia h1, y aunque también h1fue limpiada, h2todavía tiene la dirección antigua y desactualizada. El acceso a la casa que ya no está en pie podría o no funcionar.

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


Tampón desbordado

Mueves más cosas a la casa de las que puedes caber, derramando en la casa o patio de los vecinos. Cuando el dueño de esa casa vecina más tarde vuelva a casa, encontrará todo tipo de cosas que considerará suyas.

Esta es la razón por la que elegí una matriz de tamaño fijo. Para establecer el escenario, suponga que la segunda casa que asignamos, por alguna razón, se colocará antes que 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 asignados uno al lado del otro.

Por lo tanto, 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 memoria después de la primera asignación:

                        h1
                        v
----------------------- [ttttNNNNNNNNNN]
                        5678Mi casa

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

    h2 h1
    vv
--- [ttttNNNNNNNNNN] ---- [ttttNNNNNNNNNN]
    1234 Mi otra casa en algún lugar
                        ^ --- + - ^
                            El |
                            + - sobrescrito

La parte que con mayor frecuencia provocará un bloqueo es cuando sobrescribe partes importantes de los datos que almacenó que realmente no deberían modificarse al azar. Por ejemplo, podría no ser un problema que se cambiaran partes del nombre de la casa h1, en términos de bloquear el programa, pero sobreescribir la sobrecarga del objeto probablemente se bloqueará cuando intente usar el objeto roto, como lo hará sobrescribir enlaces que se almacenan en otros objetos en el objeto.


Listas enlazadas

Cuando sigue una dirección en una hoja de papel, llega a una casa, y en esa casa hay otra hoja de papel con una nueva dirección, para la siguiente casa en 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 hasta nuestra cabaña. Podemos seguir la cadena hasta que una casa no tenga NextHousereferencia, lo que significa que es la última. Para visitar todas nuestras casas, podríamos usar 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 (se agregó NextHouse como un enlace en el objeto, indicado con los cuatro LLLL en el siguiente diagrama):

    h1 h2
    vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
    1234 Casa + 5678 Cabina +
                   El | ^ |
                   + -------- + * (sin enlace)

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, el siguiente la dirección 1 y así sucesivamente. Esto es simplificado, pero lo suficientemente bueno.

Entonces este diseño de memoria:

    h1 h2
    vv
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
    1234Mi casa 5678Mi casa

Puede 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 de enlaces anterior podría verse así:

    h1 (= 4) h2 (= 28)
    vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
    1234 Inicio 0028 5678 Cabina 0000
                   El | ^ |
                   + -------- + * (sin enlace)

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


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

Un puntero es solo 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 tiempos de ejecución intenta 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 un cuadro negro, es decir. usted realmente no sabe ni le importa cómo se implementa realmente, siempre y cuando funcione.

Lasse V. Karlsen
fuente
59
El desbordamiento del búfer es hilarante. "El vecino llega a casa, rompe su cráneo y se desliza sobre tu basura, y te demanda al olvido".
gtd
12
Esta es una buena explicación del concepto, claro. Sin embargo, el concepto NO es lo que encuentro confuso sobre los punteros, por lo que todo este ensayo fue un poco desperdiciado.
Breton
10
Pero solo por haber preguntado, ¿qué te parece confuso sobre los punteros?
Lasse V. Karlsen
11
He revisado esta publicación varias veces desde que escribiste la respuesta. Su aclaración con el código es excelente, y le agradezco que lo vuelva a visitar para agregar / refinar más pensamientos. Bravo Lasse!
David McGraw
3
No hay forma de que una sola página de texto (no importa cuánto tiempo sea) pueda resumir cada matiz de la gestión de la memoria, referencias, punteros, etc. Dado que 465 personas lo han votado, diría que es lo suficientemente bueno página de inicio de la información. ¿Hay más para aprender? Claro, cuando no es así?
Lasse V. Karlsen
153

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

El profesor escribe en la pizarra: int john;

John se pone de pie

El profesor escribe: int *sally = &john;

Sally se pone de pie y señala a John

Profesor: int *bill = sally;

Bill se pone de pie y señala a John

Profesor: int sam;

Sam se pone de pie

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.

Tryke
fuente
55
No creo que me haya equivocado. Mi intención era cambiar el valor de la variable apuntada a de John a Sam. Es un poco más difícil de representar con la gente, porque parece que estás cambiando el valor de ambos indicadores.
Tryke
24
Pero la razón por la que es confuso es que no es como si John se levantara de su asiento y luego Sam se sentara, como podríamos imaginar. Es más como que Sam se acercó y metió su mano en John y clonó la programación de Sam en el cuerpo de John, como el tejido de Hugo en la matriz recargada.
Breton
59
Más como Sam toma el asiento de John, y John flota alrededor de la habitación hasta que choca con algo crítico y causa una falla.
just_wes
2
Personalmente encuentro este ejemplo innecesariamente complicado. Mi profesor me dijo que señalara una luz y dijo "tu mano es el puntero al objeto de luz".
Celeritas
El problema con este tipo de ejemplos es que el puntero a X y X no son lo mismo. Y esto no se retrata con la gente.
Isaac Nequittepas
124

Una analogía que he encontrado útil para explicar los punteros son los hipervínculos. La mayoría de la gente puede 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, siga cualquiera de esos enlaces (punteros) y obtendrá esa nueva página actualizada.

Wilka
fuente
15
Realmente me gusta esto. No es difícil ver que escribir un hipervínculo dos veces no hace que aparezcan dos sitios web (al igual int *a = bque no hace dos copias *b).
detly
44
Esto es realmente muy intuitivo y algo con lo que todos deberían poder relacionarse. Aunque hay muchos escenarios en los que esta analogía se desmorona. Excelente para una introducción rápida sin embargo. +1
Brian Wigginton
Un enlace a una página que se abre dos veces generalmente crea dos instancias casi completamente independientes de esa página web. Creo que un hipervínculo podría ser una buena analogía para un constructor, pero no para un puntero.
Utkan Gezer
@ThoAppelsin No necesariamente es cierto, si está accediendo a una página web estática html, por ejemplo, está accediendo a un solo archivo en el servidor.
dramzy
55
Lo estás pensando demasiado. Los hipervínculos apuntan a archivos en el servidor, ese es el alcance de la analogía.
dramzy
48

La razón por la cual los punteros parecen confundir a tanta gente es que en su mayoría tienen poca o ninguna experiencia en arquitectura de computadoras. 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 es pedirles que implementen una máquina virtual simple basada en bytecode (en cualquier idioma que elijan, python funciona muy bien para esto) con un conjunto de instrucciones enfocado 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 adición implicará punteros y seguramente lo obtendrán.

JSN
fuente
2
Interesante. Sin embargo, no tengo idea de cómo comenzar a hacer esto. ¿Algún recurso para compartir?
Karolis
1
Estoy de acuerdo. Por ejemplo, aprendí a programar en asamblea antes de C y sabiendo cómo funcionan los registros, aprender punteros fue fácil. De hecho, no había mucho aprendizaje, todo fue muy natural.
Milan Babuškov 01 de
Tome una CPU básica, diga algo que funcione con cortadoras de césped o lavavajillas e impleméntelo. O un subconjunto muy básico de ARM o MIPS. Ambos tienen un ISA muy simple.
Daniel Goldberg
1
Vale la pena señalar que este enfoque educativo ha sido defendido / practicado por el propio Donald Knuth. Knuth's Art of Computer Programming describe una arquitectura hipotética simple y pide a los estudiantes que implementen soluciones para practicar problemas en un lenguaje de ensamblaje hipotético para esa arquitectura. Una vez que se hizo prácticamente factible, algunos estudiantes que leen los libros de Knuth realmente implementan su arquitectura como VM (o usan una implementación existente), y realmente ejecutan sus soluciones. OMI, esta es una excelente manera de aprender, si tienes tiempo.
WeirdlyCheezy
44
@Luke No creo que sea tan fácil de entender las personas que simplemente no pueden comprender los punteros (o, para ser más exactos, la indirección en general). Básicamente, está asumiendo que las personas que no entienden los punteros en C podrían comenzar a aprender el ensamblaje, comprender la arquitectura subyacente de la computadora y volver a C con una comprensión de los punteros. Esto puede ser cierto para muchos, pero según algunos estudios, parece que algunas personas inherentemente no pueden comprender la indirecta, incluso en principio (todavía me parece muy difícil de creer, pero quizás he tenido suerte con mis "estudiantes" ").
Luaan
27

¿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 un marcador de posición para un valor - variables - se asigna a algo que nos enseñan en la escuela - álgebra. No hay un paralelismo existente que pueda dibujar sin comprender cómo la memoria está físicamente distribuida dentro de una computadora, y nadie piensa en este tipo de cosas hasta que se trata de cosas de bajo nivel, en el nivel de comunicaciones C / C ++ / byte .

¿Existen herramientas o procesos de pensamiento que lo ayudaron a comprender cómo funcionan los punteros a nivel variable, funcional y más allá?

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

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

Para un simulacro? 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);

Salida:

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

¿Quizás eso explica algunos de los conceptos básicos a través del ejemplo?

Josh
fuente
+1 para "sin comprender cómo se guarda físicamente la memoria". Llegué a C desde un entorno de lenguaje ensamblador y el concepto de punteros era muy natural y fácil; y he visto a personas con un nivel de lenguaje de nivel superior que luchan por resolverlo. Para empeorar la situación, la sintaxis es confusa (¡punteros de función!), Por lo que aprender el concepto y la sintaxis al mismo tiempo es una receta para los problemas.
Brendan
44
Sé que esta es una publicación antigua, pero sería genial si la salida del código proporcionado se agrega a la publicación.
Josh
Sí, es similar al álgebra (aunque los álgebra tienen un punto de comprensión adicional al hacer que sus "variables" sean inmutables). Pero aproximadamente la mitad de las personas que conozco no comprenden el álgebra en la práctica. Simplemente no computa para ellos. Conocen todas esas "ecuaciones" y recetas para llegar al resultado, pero las aplican de manera algo aleatoria y torpe. Y no pueden extenderlos para su propio propósito: es solo una caja negra inmutable e indestructible para ellos. Si comprende el álgebra y puede usarlo de manera efectiva, ya está muy por delante del paquete, incluso entre los programadores.
Luaan
24

La razón por la que tuve dificultades para comprender los punteros, al principio, es que muchas explicaciones incluyen mucha basura sobre pasar por referencia. Todo lo que hace es confundir el problema. Cuando usa un parámetro de puntero, todavía está pasando por valor; pero el valor es una dirección en lugar de, por ejemplo, un int.

Alguien más ya se ha vinculado a este tutorial, pero puedo resaltar el momento en que comencé a entender los punteros:

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

int puts(const char *s);

Por el momento, ignore const.El parámetro pasado a puts()es un puntero, es decir, 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 separaron y un rayo de sol me envolvió con la comprensión del puntero.

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

Kyralessa
fuente
Me deja preguntándome cuál es el punto de tener un puntero. En la mayoría de los bloques de código que encuentro, solo se ve un puntero en su declaración.
Wolfpack'08
@ Wolfpack'08 ... ¿qué? ¿Qué código estás mirando?
Kyle Strand
@KyleStrand Déjame echar un vistazo.
Wolfpack'08
19

El "Tutorial sobre punteros y matrices en C" de Ted Jensen es un excelente recurso para aprender sobre punteros. Se divide 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 de Unix, desde la cual puede comenzar a hacer cosas realmente divertidas. http://beej.us/guide/bgnet/

Ted Percival
fuente
1
Secundo el tutorial de Ted Jensen. Desglosa los punteros a un nivel de detalle, eso no es demasiado detallado, ningún libro que he leído lo hace. ¡Extramadamente útil! :)
Dave Gallagher
12

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 trozos de papel con las direcciones de las casas son herramientas de aprendizaje excelentes. Hacen un gran trabajo al presentar los conceptos básicos. De hecho, aprender los conceptos básicos es vital para utilizar con éxito los punteros. Sin embargo, en el código de producción, es común entrar en escenarios mucho más complejos de lo que pueden encapsular estas simples demostraciones.

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 punteros se vuelven realmente confusos. Si tiene múltiples niveles de indirección y comienza a terminar con un código como este:

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

puede volverse confuso realmente rápido (imagine muchas más líneas y potencialmente más niveles). Agregue conjuntos de punteros y punteros de nodo a nodo (árboles, listas vinculadas) y esto empeora aún más. He visto a algunos desarrolladores realmente buenos perderse una vez que comenzaron a trabajar en tales sistemas, incluso desarrolladores que entendieron los conceptos básicos realmente bien.

Las estructuras complejas de los punteros tampoco indican necesariamente una codificación deficiente (aunque pueden). La composición es una pieza vital de una buena programación orientada a objetos, y en lenguajes con punteros en bruto, inevitablemente conducirá a una indirección de múltiples capas. Además, los sistemas a menudo necesitan usar bibliotecas de terceros con estructuras que no coinciden entre sí en estilo o técnica. En situaciones como esa, la complejidad va a surgir naturalmente (aunque ciertamente, debemos combatirla tanto como sea posible).

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

Derek Park
fuente
10

Pensé que agregaría una analogía a esta lista que encontré muy útil al explicar los punteros (en el pasado) como Tutor de Informática; primero, vamos a:


Prepara 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}).


Agregar los datos

Un estacionamiento nunca permanece vacío por mucho tiempo ... si lo hiciera, no tendría sentido y nadie construiría ninguno. Entonces, digamos que a medida que avanza el día, el lote 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 coches son todos del mismo tipo (coche) así que una forma de pensar de esto es que nuestros coches son una especie de datos (digamos una int) pero tienen valores diferentes ( blue, red, green, que podría ser de un color enum)


Introduce el puntero

Ahora, si te llevo a este estacionamiento y te pido que me encuentres 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 puntero) no es la respuesta a mi pregunta. Buscando en el dedo me ha dicho nada, pero si miro en el que usted dedo está apuntando a (dereferencing el puntero), que se puede encontrar el coche (los datos) que estaba buscando.


Reasignando el puntero

Ahora puedo pedirte que encuentres un auto rojo y puedes redirigir tu dedo a un auto nuevo. Ahora su puntero (el mismo que antes) me muestra nuevos datos (el lugar de estacionamiento donde se puede encontrar el automóvil rojo) del mismo tipo (el automóvil).

El puntero no ha cambiado físicamente, sigue siendo tu dedo, solo cambiaron los datos que me mostraban. (la dirección del "lugar de estacionamiento")


Punteros dobles (o un puntero a un puntero)

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

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


El puntero colgante

Ahora digamos que te sientes muy parecido a una estatua, y quieres mantener tu mano apuntando al auto rojo indefinidamente. ¿Qué pasa si ese auto rojo se va?

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

El puntero sigue apuntando a que el coche rojo era pero ya no lo es. Digamos que un auto nuevo llega allí ... un auto Orange. Ahora, si te pregunto de nuevo, "¿dónde está el auto rojo", todavía estás señalando allí, pero ahora te equivocas. Eso no es un auto rojo, es naranja.


Aritmética de puntero

Ok, todavía estás apuntando al segundo lugar de estacionamiento (ahora ocupado por el auto Orange)

   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 lugar de estacionamiento. Puedes ver que estás apuntando al punto 2, así que solo agregas 1 y estás apuntando al siguiente punto. ( finger+1), ahora ya que quería saber cuáles eran los datos allí, debe verificar ese punto (no solo el dedo) para poder deducir el puntero ( *(finger+1)) para ver que hay un automóvil verde presente allí (los datos en esa ubicación )

Miguel
fuente
Simplemente no use la palabra "puntero doble". Los punteros pueden apuntar a cualquier cosa, por lo que obviamente puede tener punteros apuntando a otros punteros. No son dobles punteros.
gnasher729
Creo que esto pierde el punto de que los "dedos" mismos, para continuar con su analogía, cada uno "ocupa un lugar de estacionamiento". No estoy seguro de que las personas tengan dificultades para comprender los punteros en el alto nivel de abstracción de su analogía, es entender que los punteros son cosas mutables que ocupan ubicaciones de memoria, y cómo esto es útil, parece evadir a las personas.
Emmet
1
@Emmet: no estoy en desacuerdo con que hay mucho más que uno podría entrar en los indicadores WRT, pero leí la pregunta: "without getting them bogged down in the overall concept"como un entendimiento de alto nivel. Y a su punto: "I'm not sure that people have any difficulty understanding pointers at the high level of abstraction"- se sorprendería mucho de cuántas personas no entienden los punteros, incluso a este nivel
Mike
¿Hay algún mérito en extender la analogía del dedo del automóvil a una persona (con uno o más dedos, y una anormalidad genética que puede permitir que cada uno apunte en cualquier dirección) se sentó en uno de los autos apuntando a otro automóvil (o doblado sobre señalar el páramo al lado del lote como un "puntero no inicializado", o una mano entera extendida señalando una fila de espacios como un "conjunto de punteros de tamaño fijo [5]" o acurrucado en la palma "puntero nulo" que apunta a un lugar donde se sabe que NUNCA hay un automóvil) ... 8-)
SlySven
su explicación fue apreciable y buena para un principiante.
Yatendra Rathore
9

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

La dificultad, al menos la que he experimentado en el pasado y que he visto lidiar con otros, es que la gestión de los punteros en C / C ++ puede ser complicada sin necesidad.

Matt Mitchell
fuente
9

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

Joel Spolsky hace algunos buenos puntos sobre la comprensión de los punteros en su artículo de la Guía de Guerrilla para la Entrevista :

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

David
fuente
8

El problema con los punteros no es el concepto. Es la ejecución y el lenguaje involucrado. Se produce una confusión adicional cuando los maestros suponen que es el CONCEPTO de los punteros lo que es difícil, y no la jerga, o el complicado enredo que C y C ++ hacen del concepto. Se emplean grandes cantidades de esfuerzo para explicar el concepto (como en la respuesta aceptada para esta pregunta) y casi se desperdicia en alguien como yo, porque ya entiendo todo eso. Simplemente explica la parte incorrecta 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 los conoce como punteros. Se les conoce como direcciones. Cuando se trata de programar y usar punteros en C, cometo muchos errores y realmente me confundo. Todavía no he resuelto esto. Dejame darte un ejemplo.

Cuando una api dice:

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

que quiere

podría querer:

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

(Para decir 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 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 contiene la dirección para y" y " esta función requiere una dirección para y ". La respuesta, además, depende de en qué consiste el "mybuffer", y qué pretende hacer con él. El lenguaje no admite 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, por lo que sabe a dónde ir para modificar el contenido del puntero? La mayoría de las veces solo tengo que adivinar qué se entiende por "

"Puntero" 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 de puntero o la dirección de la variable de puntero? Estoy confundido.

bretón
fuente
Lo he visto explicado así: si ve una declaración de puntero como double *(*(*fn)(int))(char), entonces el resultado de la evaluación *(*(*fn)(42))('x')será a double. Puede quitar capas de evaluación para comprender cuáles deben ser los tipos intermedios.
Bernd Jendrissek
@BerndJendrissek No estoy seguro de seguir. ¿Cuál es el resultado de evaluar (*(*fn)(42))('x') entonces?
Breton
obtienes algo (llamémoslo x) donde, si evalúas *x, obtienes un doble.
Bernd Jendrissek
@BerndJendrissek ¿Se supone que esto explica algo sobre los punteros? No lo entiendo ¿Cual es tu punto? Me quité una capa y no obtuve información nueva sobre ningún tipo intermedio. ¿Qué explica sobre lo que una función particular va a aceptar? ¿Qué tiene que ver con algo?
Breton
Tal vez el mensaje en esta explicación (y no es mío, desearía poder encontrar dónde lo vi por primera vez) es pensarlo menos en términos de lo que fn es y más en términos de lo que puede hacer con ustedfn
Bernd Jendrissek
8

Creo que la principal barrera para entender los punteros son los malos maestros.

A casi todos se les enseñan mentiras sobre los punteros: que no son más que direcciones de memoria , o que te permiten señalar ubicaciones arbitrarias .

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

Nada de lo cual es cierto. Los punteros son en realidad conceptos bastante simples, siempre y cuando se apegue a lo que el lenguaje C ++ tiene que decir sobre ellos y no los imbuya con atributos que "generalmente" funcionan en la práctica, pero que no están garantizados por el lenguaje. , por lo que no son parte del concepto real de un puntero.

Traté de escribir una explicación de esto hace unos meses en esta publicación de blog , espero que ayude a alguien.

(Tenga en cuenta que antes de que alguien se vuelva pedante conmigo, sí, el estándar C ++ dice que los punteros representan 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 la memoria direcciones ". La distinción es importante)

jalf
fuente
Después de todo, un puntero nulo no apunta a la dirección cero en la memoria, a pesar de que su "valor" C es cero. Es un concepto completamente separado, y si lo manejas mal, podrías terminar abordando (y desreferenciando) algo que no esperabas. En algunos casos, eso podría ser incluso una dirección cero en la memoria (especialmente ahora que el espacio de direcciones es generalmente plano), pero en otros, un compilador optimizador puede omitirlo como un comportamiento indefinido o acceder a alguna otra parte de la memoria asociada con "cero" para el tipo de puntero dado. Produce hilaridad.
Luaan
No necesariamente. Debe poder modelar la computadora en su cabeza para que los punteros tengan sentido (y también para depurar otros programas). No todos pueden hacer esto.
Thorbjørn Ravn Andersen
5

Creo que lo que hace que los punteros sean difíciles de aprender es que hasta los punteros te sientes cómodo con la idea de que "en esta ubicación de memoria hay un conjunto de bits que representan un int, un doble, un personaje, lo que sea".

Cuando ve un puntero por primera vez, realmente no obtiene lo que 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 encontrar usos reales para ellos (como no pasar estructuras grandes a funciones).

Baltimark
fuente
5

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

   int *mypointer;

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

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

  int* mypointer;

Ni siquiera me hagas empezar con los punteros de funciones

burrrr
fuente
2
En realidad, ambos fragmentos son válidos C. Es cuestión de muchos años de estilo C que el primero sea más común. El segundo es bastante más común en C ++, por ejemplo.
RBerteig
1
El segundo fragmento realmente no funciona bien con declaraciones más complejas. Y la sintaxis no es tan "inconsistente" una vez que se da cuenta de que la parte derecha de una declaración de puntero le muestra lo que debe hacer con el puntero para obtener algo cuyo tipo es el especificador de tipo atómico de la izquierda.
Bernd Jendrissek
2
int *p;tiene un significado simple: *pes un número entero. int *p, **ppsignifica: *py **ppson enteros.
Miles Rout
@MilesRout: Pero ese es exactamente el problema. *py **ppson no enteros, porque nunca se ha inicializado po ppo *ppa punto para cualquier cosa. Entiendo por qué algunas personas prefieren apegarse a la gramática en este caso, particularmente porque algunos casos extremos y casos complejos requieren que lo hagas (aunque, aun así, puedes evitarlo trivialmente en todos los casos que conozco) ... pero no creo que esos casos sean más importantes que el hecho de que enseñar la alineación correcta es engañoso para los novatos. ¡Sin mencionar que es feo! :)
ligereza corre en órbita el
@LightnessRacesinOrbit Enseñar la alineación correcta está lejos de ser engañoso. Es la única forma correcta de enseñarlo. NO enseñarlo es engañoso.
Miles Rout
5

Podría trabajar con punteros cuando solo conocía C ++. Como que 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 del nivel de instrucción con un programa de lenguaje ensamblador que ha escrito, debería ser capaz de comprender muchas cosas.

toto
fuente
4

Me gusta la analogía de la dirección de la casa, pero siempre he pensado en la dirección 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 papel con la dirección 2) Vaya a la dirección en el papel 3) Abra el buzón para encontrar un nuevo pedazo de papel con la siguiente dirección.

En una lista vinculada lineal, el último buzón no tiene nada (final de la lista). En una lista circular vinculada, 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 se bloqueará o saldrá mal cuando la dirección no sea válida. Suponiendo que pueda caminar hasta el buzón de una dirección no válida, imagine que hay un agujero negro o algo allí que da la vuelta al mundo :)

Christopher Scott
fuente
Una complicación desagradable con la analogía del número de buzón es que, si bien el lenguaje inventado por Dennis Ritchie define el comportamiento en términos de las direcciones de los bytes y los valores almacenados en esos bytes, el lenguaje definido por el Estándar C invita a las implementaciones de "optimización" para usar un comportamiento modelo que es más complicado pero define varios aspectos del modelo de manera ambigua, contradictoria e incompleta.
supercat
3

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

Una vez que llega a C y C ++, parece ser más difícil para algunas personas. No estoy seguro de si esto se debe a que finalmente están poniendo en práctica la teoría de que no comprenden adecuadamente o porque la manipulación del puntero es inherentemente más difícil en esos idiomas. No recuerdo bien mi propia transición, pero conocía los indicadores en Pascal y luego me mudé a C y me perdí por completo.

Wolfbyte
fuente
2

No creo que los punteros sean confusos. La mayoría de la gente puede entender el concepto. Ahora, ¿en cuántos punteros puede pensar o con cuántos niveles de indirección se siente cómodo? No se necesitan muchos para poner a la gente al límite. El hecho de que pueden ser modificados accidentalmente por errores en su programa también puede hacer que sea muy difícil de depurar cuando las cosas salen mal en su código.

bruceatk
fuente
2

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 un iterador en la Biblioteca de plantillas estándar de c ++ . Es irónico porque solo puedo suponer que los iteradores fueron concebidos como una generalización del puntero.

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

Waylon Flinn
fuente
El problema está principalmente en la sintaxis de la declaración C. Pero el uso del puntero seguro que sería más fácil si (*p)hubiera sido (p->), y por lo tanto tendríamos que p->->xen lugar de la ambigua*p->x
MSalters
@MSalters Oh, Dios mío, estás bromeando, ¿verdad? No hay inconsistencias allí. a->bsimplemente significa (*a).b.
Miles Rout
@Miles: De hecho, y por esa lógica * p->xsignifica * ((*a).b)mientras que *p -> xsignifica (*(*p)) -> x. La combinación de operadores de prefijo y postfijo produce un análisis ambiguo.
MSalters
@MSalters no, porque el espacio en blanco es irrelevante. Es como decir que 1+2 * 3debería ser 9.
Miles Rout
2

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 las características de la arquitectura de memoria subyacente.

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

Joshua Fox
fuente
1
Lo interesante es que los punteros C en realidad no exponen ninguna característica de la arquitectura de memoria subyacente. Las únicas diferencias entre las referencias de Java y los punteros en C son que puede tener tipos complejos que involucran punteros (por ejemplo, int *** o char * ( ) (void * )), hay aritmética de puntero para matrices y punteros para estructurar miembros, la presencia del vacío * y la dualidad matriz / puntero. Aparte de eso, funcionan igual.
jpalecek
Buen punto. Es la aritmética del puntero y la posibilidad de desbordamientos del búfer, lo que lo hace salir de la abstracción al salir del área de memoria actualmente relevante.
Joshua Fox
@jpalecek: es bastante fácil entender cómo funcionan los punteros en implementaciones que documentan su comportamiento en términos de la arquitectura subyacente. Decir foo[i]significa ir a cierto lugar, avanzar una cierta distancia y ver qué hay allí. Lo que complica las cosas es la capa de abstracción adicional mucho más complicada que fue agregada por el Estándar únicamente para beneficio del compilador, pero modela las cosas de una manera que no se ajusta a las necesidades del programador y las necesidades del compilador por igual.
supercat
2

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

Así que 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 realmente solo el índice de (el primer byte de) esa variable en la RAM.

Entonces, si tiene 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 toma ese número y lo usa como un índice en sí mismo RAM[RAM[index]].

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

sashoalm
fuente
1

Número de apartado de correos.

Es una información que le permite acceder a otra cosa.

(Y si hace cálculos aritméticos en los números del apartado de correos, puede tener un problema, porque la letra va en el cuadro equivocado. Y si alguien se muda a otro estado, sin dirección de reenvío, entonces tiene un puntero colgante. Por otro lado, si la oficina de correos reenvía el correo, entonces tiene un puntero a un puntero).

joel.neely
fuente
1

No es una mala manera de entenderlo, a través de iteradores ... pero sigue buscando verás a Alexandrescu comenzar a quejarse de ellos.

Muchos desarrolladores anteriores de C ++ (que nunca entendieron que los iteradores son un puntero moderno antes de abandonar el lenguaje) saltan a C # y aún creen que tienen iteradores decentes.

Hmm, el problema es que todo lo que son los iteradores está en completo desacuerdo con lo que las plataformas de tiempo de ejecución (Java / CLR) están tratando de lograr: un uso nuevo, simple y para todos los desarrolladores. Lo que puede ser bueno, pero lo dijeron una vez en el libro púrpura y lo dijeron incluso antes y antes de C:

Indirección

Un concepto muy poderoso, pero nunca lo es si lo haces por completo. Los iteradores son útiles ya que ayudan con la abstracción de algoritmos, otro ejemplo. Y el tiempo de compilación es el lugar para un algoritmo, muy simple. Sabes código + datos, o en ese otro idioma C #:

IEnumerable + LINQ + Massive Framework = 300 MB de tiempo de ejecución penalización indirecta de pésimo, arrastrando aplicaciones a través de montones de instancias de tipos de referencia.

"Le Pointer es barato".

rama-jka toti
fuente
44
¿Qué tiene esto que ver con algo?
Neil Williams el
... ¿qué estás tratando de decir, aparte de "la vinculación estática es lo mejor" y "No entiendo cómo funciona algo diferente de lo que he aprendido anteriormente"?
Luaan
Luaan, no podrías saber lo que se puede aprender al desmontar el JIT en 2000, ¿verdad? Que termina en una tabla de salto, desde una tabla de punteros, como se muestra en 2000 en línea en ASM, por lo que no entender nada diferente puede tener otro significado: leer con cuidado es una habilidad esencial, inténtalo de nuevo.
rama-jka toti
1

Algunas respuestas anteriores han afirmado que "los punteros no son realmente difíciles", pero no se han dirigido directamente a donde "los punteros son difíciles". viene de. Hace algunos años fui tutor de estudiantes de CS de primer año (durante solo un año, ya que claramente lo chupé) y me quedó claro que la idea del puntero no es difícil. Lo difícil es entender por qué y cuándo querrías un puntero .

No creo que pueda divorciarse de esa pregunta, por qué y cuándo usar un puntero, de explicar problemas más amplios de ingeniería de software. Por qué cada variable no debería ser una variable global, y por qué uno debería factorizar un código similar en funciones (que, consiga esto, use punteros para especializar su comportamiento en su sitio de llamadas).

Bernd Jendrissek
fuente
0

No veo qué es tan confuso sobre los punteros. Apuntan a una ubicación en la memoria, es decir, almacena la dirección de 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 a 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 ir a 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).

grom
fuente
0

Solo para confundir un poco más las cosas, a veces tienes que trabajar con manijas 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 en la mitad de la rutina, los resultados son impredecibles, por lo que primero debe bloquear el controlador para asegurarse de que nada vaya a ninguna parte.

http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5 habla un poco más coherentemente que yo. :-)

SarekOfVulcan
fuente
-1: Los identificadores no son punteros a punteros; no son punteros en ningún sentido. No los confundas.
Ian Goldby
"No son punteros en ningún sentido" - mmm, ruego diferir.
SarekOfVulcan
Un puntero es una ubicación de memoria. Un identificador es cualquier identificador único. Puede ser un puntero, pero también podría ser un índice en una matriz, o cualquier otra cosa. El enlace que proporcionó es solo un caso especial en el que el identificador es un puntero, pero no tiene que ser así. Ver también parashift.com/c++-faq-lite/references.html#faq-8.8
Ian Goldby
Ese enlace no respalda su afirmación de que no son punteros en ningún sentido, tampoco: "Por ejemplo, los identificadores podrían ser Fred **, donde los punteros apuntados a Fred * ..." No creo el -1 fue justo.
SarekOfVulcan
0

Todos los principiantes de C / C ++ tienen el mismo problema y ese problema ocurre no porque "los punteros son difíciles de aprender" sino "quién y cómo se explica". Algunos alumnos lo recopilan verbalmente, algo visualmente y la mejor manera de explicarlo es usar el ejemplo de "entrenamiento" (trajes para el ejemplo verbal y visual).

Donde "locomotora" es un puntero que no puede contener nada y "vagón" es lo que "locomotora" intenta tirar (o señalar). Después, puede clasificar el "vagón" en sí, ¿puede contener animales, plantas o personas (o una mezcla de ellos).

Praveen Kumar
fuente