¿Qué les resulta difícil a las personas sobre los punteros C? [cerrado]


173

A partir de la cantidad de preguntas publicadas aquí, está claro que las personas tienen algunos problemas fundamentales para entender los punteros y la aritmética de punteros.

Tengo curiosidad por saber por qué. Realmente nunca me han causado problemas importantes (aunque aprendí por primera vez sobre ellos en el Neolítico). Para poder escribir mejores respuestas a estas preguntas, me gustaría saber qué es lo que las personas encuentran difícil.

Entonces, si estás luchando con punteros, o si recientemente te "entendiste", ¿cuáles fueron los aspectos de los punteros que te causaron problemas?


54
Snarky, exagerado, innecesariamente controvertido, responde con una pizca de verdad: han quedado paralizados --- mentalmente mutilados, te digo 'ya --- al aprender uno de ellos lenguajes de alto nivel "expresivos" primero. ¡Deberían haber comenzado programando en metal desnudo como lo que Dios un Daniel Boone pretendía!
dmckee --- ex gatito moderador

3
... y programador sería mejor, ya que se convertirá en una discusión a pesar de que más esfuerzos.
dmckee --- ex-gatito moderador

Esto es argumentativo y subjetivo, genera N resultados, mi difícil es tu fácil. Probablemente funcionará en programadores, pero aún no estamos migrando estas preguntas porque el sitio no está fuera de beta.
Sam Saffron

2
@ Sam Saffron: Si bien en general estoy de acuerdo en que se trata más de un programador, escriba la pregunta, honestamente, no sería malo si la gente estuviera dispuesta a marcar "Creo que es fácil" y "Odio ver los punteros" como spam son.
jkerian

3
Alguien tiene que mencionar esto: "Es como un dedo apuntando hacia la luna. No te concentres en el dedo o perderás toda esa gloria celestial" --Bruce Lee
mu es demasiado corto el

Respuestas:


86

Sospecho que la gente está profundizando demasiado en sus respuestas. Realmente no se requiere una comprensión de la programación, las operaciones reales de la CPU o la administración de memoria a nivel de ensamblaje.

Cuando estaba enseñando, descubrí que los siguientes agujeros en la comprensión de los estudiantes son la fuente más común de problemas:

  1. Almacenamiento de pila vs pila. Es simplemente sorprendente cuántas personas no entienden esto, incluso en un sentido general.
  2. Marcos de pila. Solo el concepto general de una sección dedicada de la pila para variables locales, junto con la razón por la que es una 'pila' ... detalles como esconder la ubicación de retorno, detalles del controlador de excepciones y registros anteriores se pueden dejar con seguridad hasta que alguien intente construir un compilador
  3. "La memoria es memoria es memoria" La conversión solo cambia qué versiones de los operadores o cuánto espacio le da el compilador para una porción particular de memoria. Sabes que estás lidiando con este problema cuando la gente habla sobre "qué es realmente la variable (primitiva) X ".

La mayoría de mis alumnos pudieron comprender un dibujo simplificado de un trozo de memoria, generalmente la sección de variables locales de la pila en el ámbito actual. En general, ayudar a dar direcciones ficticias explícitas a los distintos lugares.

Supongo que, en resumen, estoy diciendo que si quieres entender los punteros, tienes que entender las variables y lo que realmente son en las arquitecturas modernas.


13
En mi humilde opinión, la comprensión de la pila y el montón son tan innecesarios como los detalles de CPU de bajo nivel. La pila y el montón son detalles de implementación. La especificación ISO C no tiene una sola mención de la palabra "apilar" y tampoco lo hace K&R.
sigjuice

44
@sigjuice: Tus objeciones pierden el sentido tanto de la pregunta como de la respuesta. A) K&R C es un anacronismo B) ISO C no es el único lenguaje con punteros, mis puntos 1 y 2 se desarrollaron contra un lenguaje no basado en C C) El 95% de las arquitecturas (no idiomas) utilizan el montón / stack system, es bastante común que las excepciones se expliquen en relación con él. D) El punto de la pregunta era "¿por qué las personas no entienden los punteros", no "cómo explico ISO C"
jkerian

9
@John Marchetti: Aún más ... dado que la pregunta era "¿Cuál es el problema raíz con el problema de la gente con los punteros", no creo que las personas que hacen las preguntas relacionadas con el puntero estén terriblemente impresionadas con "No lo hagas" Realmente necesito saber "como respuesta. Obviamente no están de acuerdo. :)
jkerian

3
@jkerian Puede que esté desactualizado, pero las tres o cuatro páginas de K&R que explican los punteros lo hacen sin la necesidad de detalles de implementación. El conocimiento de los detalles de implementación es útil por varias razones, pero en mi humilde opinión, no debería ser un requisito previo para comprender las construcciones clave de un lenguaje.
sigjuice

3
"En general, ayudar a dar direcciones ficticias explícitas a los distintos lugares" -> +1
flujo libre

146

Cuando comencé a trabajar con ellos, el mayor problema que tuve fue la sintaxis.

int* ip;
int * ip;
int *ip;

son todos iguales.

pero:

int* ip1, ip2;  //second one isn't a pointer!
int *ip1, *ip2;

¿Por qué? porque la parte "puntero" de la declaración pertenece a la variable y no al tipo.

Y luego desreferenciar la cosa usa una notación muy similar:

*ip = 4;  //sets the value of the thing pointed to by ip to '4'
x = ip;   //hey, that's not '4'!
x = *ip;  //ahh... there's that '4'

Excepto cuando realmente necesitas obtener un puntero ... ¡entonces usas un ampersand!

int *ip = &x;

¡Hurra por la consistencia!

Luego, aparentemente solo para ser imbéciles y demostrar cuán inteligentes son, muchos desarrolladores de bibliotecas usan punteros a punteros a punteros, y si esperan una variedad de esas cosas, ¿por qué no simplemente pasar un puntero a eso también? .

void foo(****ipppArr);

para llamar a esto, necesito la dirección de la matriz de punteros a punteros a punteros de ints:

foo(&(***ipppArr));

En seis meses, cuando tenga que mantener este código, pasaré más tiempo tratando de descubrir qué significa todo esto que reescribiendo desde cero. (sí, probablemente me equivoqué con la sintaxis; ha pasado un tiempo desde que hice algo en C. Lo extraño un poco, pero luego soy un poco massoquista)


21
Tu comentario sobre el primero, >> * ip = 4; // establece el valor de ip en '4' << es incorrecto. Debería ser >> // establece el valor de la cosa apuntada por ip en '4'
aaaa bbbb

8
Apilar demasiados tipos uno encima del otro es una mala idea, en cualquier idioma. Puede encontrar escribiendo "foo (& (*** ipppArr));" extraño en C, pero escribiendo algo como "std :: map <std :: pair <int, int>, std :: pair <std :: vector <int>, std :: tuple <int, double, std :: list <int> >>> "en C ++ también es muy complejo. Esto no significa que los punteros en contenedores C o STL en C ++ sean complejos. Simplemente significa que tiene que usar mejores definiciones de tipo para que sea comprensible para el lector de su código.
Patrick

20
Sinceramente, no puedo creer que un malentendido de la sintaxis sea ​​la respuesta más votada. Esa es la parte más fácil de los punteros.
Jason

44
Incluso leyendo esta respuesta, tuve la tentación de obtener una hoja de papel y dibujar la imagen. En C, siempre estaba haciendo dibujos.
Michael Easter

19
@Jason ¿Qué medida objetiva de dificultad hay además de lo que la mayoría de las personas encuentra difícil?
Rupert Madden-Abbott

52

La comprensión adecuada de los punteros requiere conocimiento sobre la arquitectura de la máquina subyacente.

Muchos programadores de hoy no saben cómo funciona su máquina, al igual que la mayoría de las personas que saben conducir un automóvil no saben nada sobre el motor.


18
@dmckee: Bueno, ¿estoy equivocado? ¿Cuántos programadores Java podrían lidiar con un segfault?
Robert Harvey

55
¿Los segfaults tienen algo que ver con el cambio de palanca? - Un programador de Java
Tom Anderson

66
@Robert: fue un complemento genuino. Este es un tema difícil de discutir sin herir los sentimientos de las personas. Y me temo que mi comentario precipitó el mismo conflicto que pensé que había logrado evitar. Mea cupla
dmckee --- ex-gatito moderador

30
Estoy en desacuerdo; no necesita comprender la arquitectura subyacente para obtener punteros (de todos modos, son una abstracción).
Jason

11
@Jason: en C, un puntero es esencialmente una dirección de memoria. Trabajar con ellos de forma segura es imposible sin comprender la arquitectura de la máquina. Ver en.wikipedia.org/wiki/Pointer_(computing) y boredzo.org/pointers/#definition
Robert Harvey

42

Cuando se trata de punteros, las personas que se confunden están ampliamente en uno de los dos campos. He estado (¿estoy?) En ambos.

La array[]multitud

Esta es la multitud que directamente no sabe cómo traducir de la notación de puntero a la notación de matriz (o ni siquiera sabe que están relacionados). Aquí hay cuatro formas de acceder a los elementos de una matriz:

  1. notación de matriz (indexación) con el nombre de la matriz
  2. notación de matriz (indexación) con el nombre del puntero
  3. notación de puntero (el *) con el nombre del puntero
  4. notación de puntero (el *) con el nombre de la matriz

 

int vals[5] = {10, 20, 30, 40, 50};
int *ptr;
ptr = vals;

array       element            pointer
notation    number     vals    notation

vals[0]     0          10      *(ptr + 0)
ptr[0]                         *(vals + 0)

vals[1]     1          20      *(ptr + 1)
ptr[1]                         *(vals + 1)

vals[2]     2          30      *(ptr + 2)
ptr[2]                         *(vals + 2)

vals[3]     3          40      *(ptr + 3)
ptr[3]                         *(vals + 3)

vals[4]     4          50      *(ptr + 4)
ptr[4]                         *(vals + 4)

La idea aquí es que acceder a las matrices a través de punteros parece bastante simple y directo, pero de esta manera se pueden hacer un montón de cosas muy complicadas e inteligentes. Algunos de los cuales pueden dejar a los programadores experimentados de C / C ++ desconcertados, y mucho menos a los novatos sin experiencia.

El reference to a pointery pointer to a pointermultitud

Este es un gran artículo que explica la diferencia y que mencionaré y robaré un código de :)

Como un pequeño ejemplo, puede ser muy difícil ver exactamente lo que el autor quería hacer si te encuentras con algo como esto:

//function prototype
void func(int*& rpInt); // I mean, seriously, int*& ??

int main()
{
  int nvar=2;
  int* pvar=&nvar;
  func(pvar);
  ....
  return 0;
}

O, en menor medida, algo como esto:

//function prototype
void func(int** ppInt);

int main()
{
  int nvar=2;
  int* pvar=&nvar;
  func(&pvar);
  ....
  return 0;
}

Entonces, al final del día, ¿qué resolvemos realmente con todo este galimatías? Nada.

Ahora hemos visto la sintaxis de ptr-to-ptr y ref-to-ptr. ¿Hay alguna ventaja de uno sobre el otro? Me temo que no. El uso de uno de ambos, para algunos programadores, son solo preferencias personales. Algunos que usan ref-to-ptr dicen que la sintaxis es "más limpia", mientras que otros que usan ptr-to-ptr, dicen que la sintaxis ptr-to-ptr hace que sea más claro para aquellos que leen lo que está haciendo.

Esta complejidad y la aparente intercambiabilidad (en negrita) con referencias, que a menudo es otra advertencia de los punteros y un error de los recién llegados, dificulta la comprensión de los punteros. También es importante entender, por el amor de finalización, que los punteros a referencias son ilegales en C y C ++ para confundir razones que tengan en lvalue- rvaluesemántica.

Como se mencionó en una respuesta anterior, muchas veces solo tendrás estos programadores que piensan que están siendo inteligentes al usarlos ******awesome_var->lol_im_so_clever()y la mayoría de nosotros probablemente somos culpables de escribir tales atrocidades a veces, pero simplemente no es un buen código, y ciertamente no es mantenible .

Bueno, esta respuesta resultó ser más larga de lo que esperaba ...


55
Creo que puede haber dado una respuesta de C ++ a una pregunta de C aquí ... al menos la segunda parte.
detly

Los punteros a punteros también se aplican a C: p
David Titarenco

1
Je Solo veo punteros a punteros al pasar los arreglos; su segundo ejemplo no es realmente aplicable al código C más decente. Además, está arrastrando C al desorden de C ++: las referencias en C no existen.
nuevo123456

Tienes que lidiar con punteros a punteros en muchos casos legítimos. Por ejemplo, cuando se trata de un puntero de función que apunta a una función que devuelve un puntero. Otro ejemplo: una estructura que puede contener un número variable de otras estructuras. Y muchos más ...
David Titarenco 01 de

2
"los punteros a las referencias son ilegales en C" - más bien como "ausente" :)
Kos

29

Culpo personalmente a la calidad de los materiales de referencia y a las personas que enseñan; La mayoría de los conceptos en C (pero especialmente los punteros) simplemente se enseñan mal. Sigo amenazando con escribir mi propio libro en C (titulado The Last Thing The World Needs Is Another Book on The C Programming Language ), pero no tengo el tiempo ni la paciencia para hacerlo. Así que me quedo aquí y lanzo citas aleatorias del Estándar a las personas.

También está el hecho de que cuando C se diseñó inicialmente, se suponía que entendía la arquitectura de la máquina a un nivel bastante detallado solo porque no había forma de evitarla en su trabajo diario (la memoria era muy escasa y los procesadores eran muy lentos) tenías que entender cómo lo que escribiste afectó el rendimiento).


3
Si. 'int foo = 5; int * pfoo = & foo; ¿Ves lo útil que es eso? OK, avanzando ... 'Realmente no usé punteros hasta que escribí mi propia biblioteca de listas con doble enlace.
John Lopez

2
+1. Solía ​​dar clases particulares a los estudiantes de CS100 y muchos de sus problemas se resolvieron simplemente revisando los punteros de una manera comprensible.
benzado 27/10/10

1
+1 para el contexto histórico. Después de haber comenzado mucho después de ese tiempo, esto nunca se me ocurrió.
Lumi

26

Hay un gran artículo que apoya la idea de que los punteros son difíciles en el sitio de Joel Spolsky: The Perils of JavaSchools .

[Descargo de responsabilidad: no soy un enemigo de Java per se .]


2
@Jason: eso es cierto pero no niega el argumento.
Steve Townsend

44
Spolsky no dice que las JavaSchools sean la razón por la cual las personas encuentran difíciles los punteros. Él está diciendo que resultan en personas analfabetas con títulos en informática.
benzado 27/10/10

1
@benzado - punto justo - mi breve publicación mejoraría si dijera 'un gran artículo que respalde la idea de que los punteros son difíciles'. Lo que el artículo implica es que "tener un título de CS de una 'buena escuela'" no es tan buen predictor de éxito como solía ser un desarrollador, mientras que "entiende los punteros" (y la recursividad) todavía lo es.
Steve Townsend

1
@ Steve Townsend: Creo que te estás perdiendo el punto del argumento del Sr. Spolsky.
Jason

2
@Steve Townsend: el Sr. Spolsky está argumentando que las escuelas Java están criando una generación de programadores que no conocen los punteros y la recursividad, no que los punteros sean difíciles debido a la prevalencia de las escuelas Java. Como dijo "hay un gran artículo sobre por qué esto es difícil" y está vinculado a dicho artículo, parece que tiene la última interpretación. Perdóname si me equivoco.
Jason

24

La mayoría de las cosas son más difíciles de entender si no se basa en el conocimiento que está "debajo". Cuando enseñé CS se hizo mucho más fácil cuando comencé a mis alumnos a programar una "máquina" muy simple, una computadora decimal simulada con códigos de operación decimales cuya memoria consistía en registros decimales y direcciones decimales. Pondrían programas muy cortos para, por ejemplo, agregar una serie de números para obtener un total. Luego lo darían un paso para ver lo que estaba sucediendo. Podrían mantener presionada la tecla "enter" y verla correr "rápido".

Estoy seguro de que casi todos en SO se preguntan por qué es útil ser tan básico. Olvidamos lo que fue no saber programar. Jugar con una computadora de juguete de este tipo establece conceptos sin los cuales no se puede programar, como las ideas de que la computación es un proceso paso a paso, que utiliza un pequeño número de primitivas básicas para desarrollar programas y el concepto de memoria variables como lugares donde se almacenan los números, en los que la dirección o el nombre de la variable es diferente del número que contiene. Hay una distinción entre el momento en que ingresa al programa y el momento en que se "ejecuta". Considero que aprender a programar cruza una serie de "obstáculos", como programas muy simples, luego bucles y subrutinas, luego matrices, luego E / S secuenciales, luego punteros y estructura de datos.

Finalmente, al llegar a C, los indicadores son confusos, aunque K&R hizo un muy buen trabajo al explicarlos. La forma en que los aprendí en C fue saber leerlos, de derecha a izquierda. Como cuando veo int *pen mi cabeza, digo " pseñala a un int". C fue inventado como un paso adelante del lenguaje ensamblador y eso es lo que me gusta de él: está cerca de ese "terreno". Los punteros, como cualquier otra cosa, son más difíciles de entender si no tienes esa base.


1
Una buena manera de aprender esto es programar microcontroladores de 8 bits. Son fáciles de entender. Tome los controladores Atmel AVR; incluso son compatibles con gcc.
Xenu

@ Xenu: estoy de acuerdo. Para mí fue Intel 8008 y 8051 :-)
Mike Dunlavey

Para mí, era una computadora personalizada de 8 bits (el "Quizás") en el MIT en la penumbra del tiempo.
QuantumMechanic

Mike - deberías conseguir a tus estudiantes un CARDIACO :)
QuantumMechanic

1
@Quantum: CARDIAC- bueno, no había oído hablar de eso. El "Quizás", déjame adivinar, ¿fue cuando Sussman (et al) hizo que la gente leyera el libro de Mead-Conway y fabricara sus propios chips LSI? Eso fue un poco después de mi tiempo allí.
Mike Dunlavey

17

No recibí punteros hasta que leí la descripción en K&R. Hasta ese momento, los punteros no tenían sentido. Leí un montón de cosas en las que la gente decía "No aprendas consejos, son confusos y te harán daño en la cabeza y te causarán aneurismas", así que me alejé por mucho tiempo y creé este aire innecesario de concepto difícil. .

De lo contrario, sobre todo lo que pensé fue, ¿por qué querrías una variable por la que tienes que pasar por los aros para obtener el valor, y si querías asignarle cosas, tenías que hacer cosas extrañas para obtener valores? en ellas. Todo el punto de una variable es algo para almacenar un valor, pensé, así que por qué alguien quería complicarlo estaba más allá de mí. "Entonces, ¿con un puntero tienes que usar el *operador para obtener su valor? ¿Qué tipo de variable tonta es esa?" , Pensé. No tiene sentido, sin juego de palabras.

La razón por la que fue complicado fue porque no entendí que un puntero era la dirección de algo. Si explica que es una dirección, que es algo que contiene una dirección a otra cosa y que puede manipular esa dirección para hacer cosas útiles, creo que podría aclarar la confusión.

Una clase que requería usar punteros para acceder / modificar puertos en una PC, usar aritmética de punteros para abordar diferentes ubicaciones de memoria y observar códigos C más complicados que modificaron sus argumentos me desilusionó de la idea de que los punteros eran, bueno, inútiles.


44
Si tiene recursos limitados para trabajar (RAM, ROM, CPU), como en aplicaciones integradas, los punteros rápidamente tienen mucho más sentido.
Nick T

+1 para el comentario de Nick, especialmente para pasar estructuras.
nuevo123456

12

Aquí hay un ejemplo de puntero / matriz que me dio pausa. Suponga que tiene dos matrices:

uint8_t source[16] = { /* some initialization values here */ };
uint8_t destination[16];

Y su objetivo es copiar el contenido de uint8_t del destino de origen usando memcpy (). Adivina cuál de los siguientes logra ese objetivo:

memcpy(destination, source, sizeof(source));
memcpy(&destination, source, sizeof(source));
memcpy(&destination[0], source, sizeof(source));
memcpy(destination, &source, sizeof(source));
memcpy(&destination, &source, sizeof(source));
memcpy(&destination[0], &source, sizeof(source));
memcpy(destination, &source[0], sizeof(source));
memcpy(&destination, &source[0], sizeof(source));
memcpy(&destination[0], &source[0], sizeof(source));

La respuesta (Spoiler Alert!) Es TODAS ellas. "destination", "& destination" y "& destination [0]" tienen el mismo valor. "& destino" es un tipo diferente que los otros dos, pero sigue siendo el mismo valor. Lo mismo ocurre con las permutaciones de "fuente".

Como comentario aparte, personalmente prefiero la primera versión.


Prefiero la primera versión también (menos puntuación).
sigjuice

++ Yo también, pero realmente debes tener cuidado sizeof(source), porque si sourcees un puntero, sizeofno será lo que quieres. A veces (no siempre) escribo sizeof(source[0]) * number_of_elements_of_sourcesolo para mantenerme alejado de ese error.
Mike Dunlavey

destination, & destination, & destination [0] no son iguales, pero cada uno de ellos mediante un mecanismo diferente se convertirá al mismo vacío * cuando se use en memcpy. Sin embargo, cuando se usa como argumento de sizeof, obtendrá dos resultados diferentes y es posible obtener tres resultados diferentes.
gnasher729

Pensé que se requería la dirección del operador?
MarcusJ

7

Debería comenzar diciendo que C y C ++ fueron los primeros lenguajes de programación que aprendí. Comencé con C, luego hice C ++ en la escuela, mucho, y luego volví a C para hablarlo con fluidez.

Lo primero que me confundió acerca de los punteros al aprender C fue lo simple:

char ch;
char str[100];
scanf("%c %s", &ch, str);

Esta confusión se debió principalmente a haber sido introducida al uso de referencias a una variable para argumentos OUT antes de que los punteros me fueran presentados correctamente. Recuerdo que omití escribir los primeros ejemplos en C para Dummies porque eran demasiado simples solo para nunca tener el primer programa que escribí para trabajar (muy probablemente debido a esto).

Lo que era confuso sobre esto era lo que &chrealmente significaba y por qué strno lo necesitaba.

Después de familiarizarme con eso, recuerdo haber estado confundido sobre la asignación dinámica. En algún momento me di cuenta de que tener punteros a los datos no era extremadamente útil sin una asignación dinámica de algún tipo, así que escribí algo como:

char * x = NULL;
if (y) {
     char z[100];
     x = z;
}

para tratar de asignar dinámicamente algo de espacio. No funcionó. No estaba seguro de que funcionaría, pero no sabía cómo podría funcionar.

Más tarde aprendí acerca de mallocy new, pero realmente me parecieron generadores de memoria mágica. No sabía nada sobre cómo podrían funcionar.

Algún tiempo después, otra vez me enseñaron la recursión (lo había aprendido por mi cuenta antes, pero ahora estaba en clase) y pregunté cómo funcionaba bajo el capó, dónde estaban almacenadas las variables separadas. Mi profesor dijo "en la pila" y muchas cosas me quedaron claras. Había escuchado el término antes y había implementado pilas de software antes. Había escuchado a otros referirse a "la pila" mucho antes, pero lo había olvidado.

Alrededor de este tiempo también me di cuenta de que usar matrices multidimensionales en C puede ser muy confuso. Sabía cómo funcionaban, pero era tan fácil enredarse en eso que decidí tratar de evitar usarlos siempre que pudiera. Creo que el problema aquí era principalmente sintáctico (especialmente al pasar o devolverlos de las funciones).

Como estaba escribiendo C ++ para la escuela durante el próximo año o dos, obtuve mucha experiencia en el uso de punteros para estructuras de datos. Aquí tuve un nuevo conjunto de problemas: mezclar punteros. Me gustaría que varios niveles de punteros (cosas como node ***ptr;) me hicieran tropezar. Quitaría la referencia de un puntero la cantidad incorrecta de veces y eventualmente recurriría a calcular cuántas* necesitaba por prueba y error.

En algún momento aprendí cómo funcionaba el montón de un programa (más o menos, pero lo suficientemente bueno como para que ya no me mantuviera despierto por la noche). Recuerdo haber leído que si mira unos pocos bytes antes de que mallocregrese el puntero que en cierto sistema, puede ver cuántos datos se asignaron realmente. Me di cuenta de que el código en mallocpodría pedir más memoria del sistema operativo y esta memoria no era parte de mis archivos ejecutables. Tener una idea funcional decente de cómo mallocfunciona es realmente útil.

Poco después de esto, tomé una clase de asamblea, que no me enseñó tanto sobre punteros como la mayoría de los programadores probablemente piensan. Me hizo pensar más sobre en qué ensamblado podría traducirse mi código. Siempre había tratado de escribir código eficiente, pero ahora tenía una mejor idea de cómo hacerlo.

También tomé un par de clases donde tuve que escribir un poco de lisp . Al escribir lisp, no estaba tan preocupado por la eficiencia como en C. Tenía muy poca idea de a qué se podría traducir este código si se compilaba, pero sabía que parecía utilizar muchos símbolos (variables) locales con nombre. Las cosas son mucho más fáciles. En algún momento escribí un código de rotación de árbol AVL en un poco de ceceo, que me costó mucho escribir en C ++ debido a problemas con el puntero. Me di cuenta de que mi aversión a lo que pensaba que eran variables locales en exceso había impedido mi capacidad de escribir eso y varios otros programas en C ++.

También tomé una clase de compiladores. Mientras estaba en esta clase, pasé al material avanzado y aprendí sobre la asignación única estática (SSA) y las variables muertas, lo cual no es tan importante, excepto que me enseñó que cualquier compilador decente hará un trabajo decente al tratar con variables que son ya no se usa Ya sabía que más variables (incluidos los punteros) con los tipos correctos y los buenos nombres me ayudarían a mantener las cosas claras en mi cabeza, pero ahora también sabía que evitarlas por razones de eficiencia era aún más estúpido de lo que me dijeron mis profesores menos optimistas. yo.

Entonces, para mí, conocer un poco sobre el diseño de memoria de un programa me ayudó mucho. Pensar en lo que significa mi código, tanto simbólicamente como en el hardware, me ayuda. El uso de punteros locales que tienen el tipo correcto ayuda mucho. A menudo escribo código que se parece a:

int foo(struct frog * f, int x, int y) {
    struct leg * g = f->left_leg;
    struct toe * t = g->big_toe;
    process(t);

así que si arruino un tipo de puntero, por el error del compilador queda muy claro cuál es el problema. Si lo hice:

int foo(struct frog * f, int x, int y) {
    process(f->left_leg->big_toe);

y si hay algún tipo de puntero incorrecto allí, el error del compilador sería mucho más difícil de resolver. Me sentiría tentado a recurrir a cambios de prueba y error en mi frustración, y probablemente empeoraría las cosas.


1
+1. Completo y perspicaz. Me había olvidado del scanf, pero ahora que lo mencionas, recuerdo haber tenido la misma confusión.
Joe White

6

Mirando hacia atrás, había cuatro cosas que realmente me ayudaron a comprender finalmente los punteros. Antes de esto, podía usarlos, pero no los entendía completamente. Es decir, sabía que si seguía los formularios, obtendría los resultados que deseaba, pero no entendía completamente el "por qué" de los formularios. Me doy cuenta de que esto no es exactamente lo que ha pedido, pero creo que es un corolario útil.

  1. Escribir una rutina que tomara un puntero a un entero y modificara el entero. Esto me dio las formas necesarias sobre las cuales construir modelos mentales de cómo funcionan los punteros.

  2. Asignación de memoria dinámica unidimensional. Descubrir la asignación de memoria 1-D me hizo comprender el concepto del puntero.

  3. Asignación de memoria dinámica bidimensional. Descubrir la asignación de memoria en 2-D reforzó ese concepto, pero también me enseñó que el puntero en sí mismo requiere almacenamiento y debe tenerse en cuenta.

  4. Diferencias entre variables de pila, variables globales y memoria de montón. Descubrir estas diferencias me enseñó los tipos de memoria a los que apuntan / se refieren los punteros.

Cada uno de estos elementos requería imaginar lo que estaba sucediendo en un nivel inferior: construir un modelo mental que satisficiera cada caso que se me ocurriera. Tomó tiempo y esfuerzo, pero valió la pena. Estoy convencido de que para comprender los punteros, debe construir ese modelo mental sobre cómo funcionan y cómo se implementan.

Ahora volvamos a su pregunta original. Según la lista anterior, había varios elementos que tuve dificultades para comprender originalmente.

  1. Cómo y por qué se usaría un puntero.
  2. ¿Cómo son diferentes y, sin embargo, similares a las matrices?
  3. Comprender dónde se almacena la información del puntero.
  4. Comprender qué y dónde está apuntando el puntero.

Oye, ¿podrías señalarme un artículo / libro / tu dibujo / garabato / cualquier cosa donde pueda aprender de una manera similar a la que describiste en tu respuesta? Creo firmemente que este es el camino a seguir cuando se aprende bien, básicamente cualquier cosa. Profunda comprensión y buenos modelos mentales
Alexander Starbuck

1
@AlexStarbuck: no quiero decir que esto suene impertinente, pero el método científico es una gran herramienta. Hazte un dibujo de lo que crees que podría suceder en un escenario particular. Programe algo para probarlo y analice lo que obtuvo. ¿Coincidió con lo que esperabas? Si no, ¿identifica dónde difiere? Repita según sea necesario, aumentando gradualmente la complejidad para evaluar tanto su comprensión como sus modelos mentales.
Sparky

6

Tuve mi "momento de puntero" trabajando en algunos programas de telefonía en C. Tuve que escribir un emulador de intercambio AXE10 usando un analizador de protocolos que solo entendía el clásico C. Todo dependía de conocer punteros. Traté de escribir mi código sin ellos (hey, estaba "pre-puntero" me cortó un poco) y fallé por completo.

La clave para entenderlos, para mí, fue el operador & (dirección). Una vez que entendí que &isignificaba la "dirección de i", entonces entendí que *isignificaba "el contenido de la dirección señalada por i" llegó un poco más tarde. Cada vez que escribía o leía mi código, siempre repetía lo que significaba "&" y lo que significaba "*" y eventualmente llegué a usarlos intuitivamente.

Para mi vergüenza, me forzaron a usar VB y luego Java, por lo que mi conocimiento de punteros no es tan agudo como lo era antes, pero me alegro de haber sido "post-puntero". Sin embargo, no me pidas que use una biblioteca que requiera que comprenda * * p.


Si &ies la dirección y *iel contenido, ¿cuál es i?
Thomas Ahle

2
Estoy sobrecargando el uso de i. Para una variable arbitraria i, & i significa "la dirección de" i, i por sí mismo significa "el contenido de & i", y * i significa "tratar el contenido de & i como una dirección, ir a esa dirección y devolver el contenido".
Gary Rowe

5

La principal dificultad con los punteros, al menos para mí, es que no comencé con C. Empecé con Java. Toda la noción de punteros era realmente extranjera hasta un par de clases en la universidad donde se esperaba que conociera C. Entonces, me enseñé los conceptos básicos de C y cómo usar los punteros en su sentido más básico. Incluso entonces, cada vez que me encuentro leyendo el código C, tengo que buscar la sintaxis del puntero.

Entonces, en mi experiencia muy limitada (1 año en el mundo real + 4 en la universidad), los indicadores me confunden porque nunca he tenido que usarlo realmente en otra cosa que no sea un aula. Y puedo simpatizar con los estudiantes que ahora comienzan CS con JAVA en lugar de C o C ++. Como dijiste, aprendiste punteros en la era 'neolítica' y probablemente lo has estado usando desde entonces. Para nosotros, las personas más nuevas, la noción de asignar memoria y hacer aritmética de punteros es realmente extraña porque todos estos lenguajes lo han abstraído.

PD: Después de leer el ensayo de Spolsky, su descripción de 'JavaSchools' no se parecía en nada a lo que pasé en la universidad de Cornell ('05 -'09). Tomé las estructuras y la programación funcional (sml), los sistemas operativos (C), los algoritmos (lápiz y papel) y una gran cantidad de otras clases que no se enseñaron en Java. Sin embargo, todas las clases introductorias y electivas se realizaron en Java porque tiene valor no reinventar la rueda cuando intentas hacer algo más nivelado que implementar una tabla hash con punteros.


44
Honestamente, dado que todavía tiene dificultades con los punteros, no estoy seguro de que su experiencia en Cornell contradiga sustancialmente el artículo de Joel. Obviamente, suficiente de su cerebro está conectado en una mentalidad de Java para hacer su punto.
jkerian

55
Wat? Las referencias en Java (o C #, o Python, o probablemente docenas de otros lenguajes) son solo punteros sin la aritmética. Punteros comprensión significa entender por qué void foo(Clazz obj) { obj = new Clazz(); }es un no-op, mientras que void bar(Clazz obj) { obj.quux = new Quux(); }muta el argumento ...

1
Sé qué referencias hay en Java, pero solo digo que si me pediste que hiciera una reflexión en Java o escribiera algo significativo en CI, no puedo sacarlo. Requiere mucha investigación, como aprenderlo por primera vez, siempre.
shoebox639

1
¿Cómo es que pasaste por una clase de sistemas operativos en C sin tener fluidez en C? Sin ánimo de ofender, es solo que recuerdo haber tenido que desarrollar un sistema operativo simple desde cero. Debo haber usado punteros mil veces ...
Gravity

5

Aquí hay una no respuesta: use cdecl (o c ++ decl) para resolverlo:

eisbaw@leno:~$ cdecl explain 'int (*(*foo)(const void *))[3]'
declare foo as pointer to function (pointer to const void) returning pointer to array 3 of int

4

Añaden una dimensión adicional al código sin un cambio significativo en la sintaxis. Piensa sobre esto:

int a;
a = 5

Sólo hay una cosa a cambio: a. Puedes escribir a = 6y los resultados son obvios para la mayoría de las personas. Pero ahora considere:

int *a;
a = &some_int;

Hay dos cosas aque son relevantes en diferentes momentos: el valor real de a, el puntero y el valor "detrás" del puntero. Puedes cambiar a:

a = &some_other_int;

... y some_inttodavía está en algún lugar con el mismo valor. Pero también puede cambiar lo que señala:

*a = 6;

Hay una brecha conceptual entre a = 6, que solo tiene efectos secundarios locales, y *a = 6, que podría afectar un montón de otras cosas en otros lugares. Mi punto aquí es que no , que el concepto de direccionamiento indirecto es de por sí difícil, pero que debido a que se puede hacer tanto en lo inmediato, local con ao indirecta con la cosa *a... que podría ser lo que confunde a la gente.


4

Había programado en c ++ durante aproximadamente 2 años y luego me convertí a Java (5 años) y nunca miré hacia atrás. Sin embargo, cuando recientemente tuve que usar algunas cosas nativas, descubrí (con asombro) que no había olvidado nada sobre los punteros e incluso los encuentro fáciles de usar. Este es un fuerte contraste con lo que experimenté hace 7 años cuando traté de comprender el concepto. Entonces, ¿supongo que entender y gustar es una cuestión de madurez de programación? :)

O

Los punteros son como andar en bicicleta, una vez que descubres cómo trabajar con ellos, no hay que olvidarlo.

En general, difícil de entender o no, la idea del puntero es muy educativa y creo que todos los programadores deben entenderla, independientemente de si programa en un idioma con punteros o no.


3

Los punteros son difíciles debido a la indirección.


"Se dice que no hay ningún problema en ciencias de la computación que no pueda resolverse con un nivel más de indirección" (sin embargo, no tengo idea de quién lo dijo primero)
The Archetypal Paul

Es como magia, donde la mala dirección es lo que confunde a las personas (pero es totalmente increíble)
Nick T

3

Los punteros (junto con algunos otros aspectos del trabajo de bajo nivel) requieren que el usuario quite la magia.

A la mayoría de los programadores de alto nivel les gusta la magia.


3

Los punteros son una forma de tratar la diferencia entre un identificador de un objeto y un objeto en sí. (ok, no necesariamente objetos, pero sabes a qué me refiero, así como dónde está mi mente)

En algún momento, probablemente tengas que lidiar con la diferencia entre los dos. En el lenguaje moderno de alto nivel, esto se convierte en la distinción entre copia por valor y copia por referencia. De cualquier manera, es un concepto que a menudo es difícil de entender para los programadores.

Sin embargo, como se ha señalado, la sintaxis para manejar este problema en C es fea, inconsistente y confusa. Eventualmente, si realmente intentas entenderlo, un puntero tendrá sentido. Pero cuando comienzas a lidiar con punteros a punteros, y así sucesivamente hasta la náusea, se vuelve realmente confuso para mí y para otras personas.

Otra cosa importante para recordar acerca de los punteros es que son peligrosos. C es el lenguaje de un maestro programador. Asume que sabes qué diablos estás haciendo y, por lo tanto, te da el poder de realmente arruinar las cosas. Si bien algunos tipos de programas aún deben escribirse en C, la mayoría de los programas no, y si tiene un lenguaje que proporcione una mejor abstracción de la diferencia entre un objeto y su identificador, le sugiero que lo use.

De hecho, en muchas aplicaciones modernas de C ++, a menudo ocurre que cualquier aritmética de puntero requerida se encapsula y se abstrae. No queremos que los desarrolladores hagan aritmética de punteros por todas partes. Queremos una API centralizada y bien probada que haga aritmética de punteros en el nivel más bajo. Hacer cambios a este código debe hacerse con mucho cuidado y pruebas exhaustivas.


3

Creo que una razón por la cual los punteros en C son difíciles es porque combinan varios conceptos que no son realmente equivalentes; sin embargo, debido a que todos se implementan utilizando punteros, las personas pueden tener dificultades para desenredar los conceptos.

En C, los punteros están acostumbrados a, entre otras cosas:

  • Definir estructuras de datos recursivas.

En C definirías una lista vinculada de enteros como este:

struct node {
  int value;
  struct node* next;
}

El puntero solo está allí porque esta es la única forma de definir una estructura de datos recursiva en C, cuando el concepto realmente no tiene nada que ver con un detalle de tan bajo nivel como las direcciones de memoria. Considere el siguiente equivalente en Haskell, que no requiere el uso de punteros:

data List = List Int List | Null

Bastante sencillo: una lista está vacía o se forma a partir de un valor y el resto de la lista.

  • Iterar sobre cadenas y matrices

Así es como puede aplicar una función fooa cada carácter de una cadena en C:

char *c;
for (c = "hello, world!"; *c != '\0'; c++) { foo(c); }

A pesar de utilizar también un puntero como iterador, este ejemplo tiene muy poco en común con el anterior. Crear un iterador que puede incrementar es un concepto diferente de definir una estructura de datos recursiva. Ningún concepto está especialmente vinculado a la idea de una dirección de memoria.

  • Lograr polimorfismo

Aquí hay una firma de función real que se encuentra en glib :

typedef struct g_list GList;

void  g_list_foreach    (GList *list,
                 void (*func)(void *data, void *user_data),
                         void* user_data);

Whoa! Eso es bastante bocado void*. Y todo es solo para declarar una función que itera sobre una lista que puede contener cualquier tipo de cosa, aplicando una función a cada miembro. Compárelo con cómo mapse declara en Haskell:

map::(a->b)->[a]->[b]

Eso es mucho más sencillo: mapes una función que toma una función que convierte una aa b, y la aplica a una lista de a's para producir una lista de b' s. Al igual que en la función C g_list_foreach, mapno necesita saber nada en su propia definición sobre los tipos a los que se aplicará.

Para resumir:

Creo que los punteros en C serían mucho menos confusos si las personas aprendieran primero sobre estructuras de datos recursivas, iteradores, polimorfismo, etc., como conceptos separados, y luego aprendieran cómo se pueden usar los punteros para implementar esas ideas en C , en lugar de combinar todos estos conceptos juntos en un solo tema de "punteros".


Uso indebido de c != NULLsu ejemplo de "Hola mundo" ... quiere decir *c != '\0'.
Olaf Seibert

2

Creo que requiere una base sólida, probablemente desde el nivel de la máquina, con introducción a algunos códigos de máquina, ensamblaje y cómo representar elementos y estructura de datos en la RAM. Toma un poco de tiempo, algo de tarea o práctica para resolver problemas, y algo de pensamiento.

Pero si una persona conoce idiomas de alto nivel al principio (lo cual no tiene nada de malo: un carpintero usa un hacha. Una persona que necesita dividir el átomo usa otra cosa. Necesitamos personas que sean carpinteros, y tenemos personas que estudian átomos) y A esta persona que conoce un lenguaje de alto nivel se le da una introducción de 2 minutos a los punteros, y luego es difícil esperar que comprenda la aritmética de punteros, punteros a punteros, matriz de punteros a cadenas de tamaño variable y matriz de matriz de caracteres, etc. Una base sólida de bajo nivel puede ayudar mucho.


2
Los punteros de Groking no requieren una comprensión del código de máquina o ensamblaje.
Jason

Requerir, no. Pero las personas que entienden el ensamblaje probablemente encontrarán los indicadores mucho, mucho más fáciles, ya que ya han hecho la mayoría (si no todas) de las conexiones mentales necesarias.
cHao el

2

El problema que siempre he tenido (principalmente autodidacta) es el "cuándo" usar un puntero. Puedo entender la sintaxis para construir un puntero, pero necesito saber en qué circunstancias se debe usar un puntero.

¿Soy el único con esta mentalidad? ;-)


Lo entiendo. Mi respuesta trata un poco con eso.
J. Polfer

2

Érase una vez ... Teníamos microprocesadores de 8 bits y todos escribieron en conjunto. La mayoría de los procesadores incluían algún tipo de direccionamiento indirecto utilizado para tablas de salto y núcleos. Cuando aparecieron los lenguajes de nivel superior, agregamos una capa delgada de abstracción y los llamamos punteros. Con los años nos hemos alejado cada vez más del hardware. Este no es necesariamente algo malo. Se llaman idiomas de nivel superior por una razón. Cuanto más me pueda concentrar en lo que quiero hacer en lugar de los detalles de cómo se hace, mejor.


2

Parece que muchos estudiantes tienen un problema con el concepto de indirección, especialmente cuando se encuentran con el concepto de indirección por primera vez. Recuerdo que cuando era estudiante, de los más de 100 estudiantes de mi curso, solo un puñado de personas realmente entendía los consejos.

El concepto de indirección no es algo que usamos a menudo en la vida real y, por lo tanto, es un concepto difícil de entender inicialmente.


2

Recientemente tuve el momento de hacer clic con el puntero y me sorprendió haberlo encontrado confuso. Era más que todos hablaban tanto de eso, que supuse que estaba ocurriendo algo de magia oscura.

La forma en que lo obtuve fue esto. Imagine que todas las variables definidas tienen espacio de memoria en tiempo de compilación (en la pila). Si desea un programa que pueda manejar archivos de datos grandes como audio o imágenes, no querría una cantidad fija de memoria para estas estructuras potenciales. Así que espera hasta el tiempo de ejecución para asignar una cierta cantidad de memoria para mantener estos datos (en el montón).

Una vez que tenga sus datos en la memoria, no querrá copiar esos datos en todo su bus de memoria cada vez que desee ejecutar una operación en él. Supongamos que desea aplicar un filtro a sus datos de imagen. Tiene un puntero que comienza en la parte frontal de los datos que ha asignado a la imagen, y una función se ejecuta a través de esos datos, cambiándolos en su lugar. Si no supiera lo que estamos haciendo, probablemente terminaría haciendo duplicados de datos, mientras los ejecutaba a través de la operación.

¡Al menos así lo veo yo en este momento!


Como un pensamiento posterior, podría definir una cantidad fija de memoria para almacenar imágenes / audio / video, digamos en un dispositivo con memoria limitada, pero luego tendría que lidiar con algún tipo de solución de transmisión dentro y fuera de la memoria.
Chris Barry

1

Hablando como un novato en C ++ aquí:

El sistema de puntero tardó un tiempo en digerir no necesariamente por el concepto sino por la sintaxis de C ++ en relación con Java. Algunas cosas que encontré confusas son:

(1) Declaración variable:

A a(1);

vs.

A a = A(1);

vs.

A* a = new A(1); 

y aparentemente

A a(); 

es una declaración de función y no una declaración de variable. En otros idiomas, básicamente hay una sola forma de declarar una variable.

(2) El ampersand se usa de diferentes maneras. Si esto es

int* i = &a;

entonces el & a es una dirección de memoria.

OTOH, si es

void f(int &a) {}

entonces el & a es un parámetro pasado por referencia.

Aunque esto puede parecer trivial, puede ser confuso para los nuevos usuarios: vengo de Java y Java, un lenguaje con un uso más uniforme de los operadores.

(3) Relación matriz-puntero

Una cosa que es un poco frustrante de comprender es que un puntero

int* i

puede ser un puntero a un int

int *i = &n; // 

o

puede ser una matriz para un int

int* i = new int[5];

Y luego, para complicar aún más las cosas, los punteros y la matriz no son intercambiables en todos los casos y los punteros no se pueden pasar como parámetros de la matriz.

Esto resume algunas de las frustraciones básicas que tuve con C / C ++ y sus punteros, que en mi opinión, se agrava en gran medida por el hecho de que C / C ++ tiene todas estas peculiaridades específicas del lenguaje.


C ++ 2011 ha mejorado bastante las cosas en lo que respecta a las declaraciones de variables.
gnasher729

0

Personalmente no entendí el puntero incluso después de mi graduación y después de mi primer trabajo. Lo único que sabía es que lo necesita para listas enlazadas, árboles binarios y para pasar matrices a funciones. Esta era la situación incluso en mi primer trabajo. Solo cuando comencé a dar entrevistas, entiendo que el concepto de puntero es profundo y tiene un uso y potencial tremendos. Luego comencé a leer K&R y escribir mi propio programa de prueba. Todo mi objetivo fue impulsado por el trabajo.
En este momento descubrí que los indicadores no son realmente malos ni difíciles si se les enseña de una buena manera. Desafortunadamente, cuando aprendo C en la graduación, nuestro maestro no era consciente del puntero, e incluso las tareas usaban menos punteros. En el nivel de posgrado, el uso del puntero es realmente solo para crear árboles binarios y listas vinculadas. Esta idea de que no necesita una comprensión adecuada de los punteros para trabajar con ellos, mata la idea de aprenderlos.


0

Punteros ... ja ... todo sobre el puntero en mi cabeza es que da una dirección de memoria donde los valores reales de cualquiera sea su referencia ... así que no hay magia al respecto ... si aprendes algo de ensamblaje no tendrías tantos problemas para aprender cómo funcionan los punteros ... vamos chicos ... incluso en Java todo es una referencia ...


0

El principal problema que las personas no entienden es por qué necesitan punteros. Porque no tienen claro el apilamiento y el montón. Es bueno comenzar desde un ensamblador de 16 bits para x86 con un modo de memoria pequeña. Ayudó a muchas personas a hacerse una idea de la pila, el montón y la "dirección". Y byte :) Los programadores modernos a veces no pueden decirle cuántos bytes necesita para direccionar el espacio de 32 bits. ¿Cómo pueden hacerse una idea de los punteros?

El segundo momento es la notación: declaras el puntero como *, obtienes la dirección como & y esto no es fácil de entender para algunas personas.

Y lo último que vi fue un problema de almacenamiento: entienden el montón y la pila, pero no pueden tener una idea de "estática".

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.