Recientemente comencé a aprender programación por mi cuenta y los punteros me resultaron bastante fáciles. ¿Por qué la gente dice que los punteros son complejos? ¿Me equivoco y esto es solo el efecto Dunning-Kruger?

Su idea no está mal, pero en cierto modo está incompleta. Un puntero esencialmente encapsula dos piezas de información separadas: . Piense en toda la memoria (virtual, generalmente) como una cinta plana dividida en una matriz lineal de celdas de igual tamaño con cada celda con una dirección o identificador único. Para casi cualquier arquitectura en la que va a escribir programas, cada celda tendrá un byte de longitud. Las celdas adyacentes llevan números consecutivos como direcciones. El tamaño es en realidad el tamaño del tipo de datos (es decir, su longitud medida en número de celdas) al que apunta el puntero. Esto es crítico para la aritmética del puntero. Por ejemplo:

int * a, // 4 bytes de longitud.
a ++; // dirección original señalada por un + 4.
char * b; // 1 byte de largo
b ++; // dirección original señalada por b + 1.

Esto es muy importante porque en C, los punteros y las matrices son esencialmente las mismas cosas. Más precisamente, las matrices son punteros constantes. Los siguientes códigos son equivalentes:

int a [10];
int * b = a; // dirección de a [0] almacenada en b.
int c = b [5]; // quinto elemento de un almacenado en c.
int d = * (b + 5); // quinto elemento de un almacenado en d.
int e = a [5]; // quinto elemento de un almacenado en e.

Con anotaciones como esta, sin embargo, surgen complicaciones. Por ejemplo, si alguien hizo lo siguiente:

* (a + 1);

En realidad, esto lo llevará al undécimo elemento de a y no al segundo elemento como esperaba, ya que el tamaño de a es 10 * sizeof(int) . Pero como solo tiene 10 elementos, dicho código se comportará de manera impredecible.

Como puede ver, es fácil para un programador pasar por alto algo y cometer errores. De hecho, los principales problemas que vienen con la programación usando punteros es la depuración cuando ocurren errores. En mi experiencia como programador en C, los siguientes son algunos de los problemas comunes que tienen que enfrentar las personas que trabajan con punteros.

A) Punteros no inicializados.

Este es el tipo más común de error con punteros. Tenga en cuenta que esto no es lo mismo que los punteros NULL. Un programador declara un puntero dentro de una función y se olvida de inicializarlo antes de desreferenciarlo, así.

vacío foo ()
{
int * a; // sin inicializar.
int b = * a;
}

Esto es más común de lo que piensa, especialmente cuando la inicialización ocurre en diferentes puntos dentro de las declaraciones de rama en una función compleja y el programador pasa por alto la inicialización dentro de una de las ramas. Cuando esto sucede, su programa esencialmente se comporta de manera impredecible. La razón es que las variables declaradas dentro de las funciones pueden contener cualquier valor (a diferencia de las declaradas fuera de las cuales se garantiza que son NULL ). Este ‘cualquier valor’ se llama formalmente un valor basura, que es básicamente el contenido de ese bloque de memoria física en ese momento . Si esa dirección apunta accidentalmente a una dirección que no está asignada a su programa, entonces su programa se desconectará y morirá. Si termina apuntando a una variable existente en el programa o bloque asignado de memoria virtual, entonces podría terminar modificando esa variable o bloque de memoria en su lugar.

Tal vez porque el valor de la basura no siempre es una dirección no asignada, y tal vez esa rama de código en particular es rara, tal error puede estar presente en el código durante años, incluso en la producción antes de que ocurra algo malo (historia real).

B) Doble indirección.

A muchas personas que conozco que no programan mucho les resulta muy difícil entender esto. En esencia, una declaración como int **a . Lo que significa es que a es un puntero que apunta a un puntero int ( *a ). Es poco común declarar dicha variable a nivel local o global, pero es muy común en el paso de parámetros. Considere la siguiente función:

void find_last_elem (int * arr, int num_elem, int ** last_elem)
{
* last_elem = & arr [num_elem – 1];
}

Lo que hace esta función es establecer el puntero *last_elem para que apunte al último elemento de la matriz, cuya dirección es el primer elemento arr . Esto quedará claro cuando vea cómo se invoca la función.

int arr [10], * último;
find_last_elem (arr, 10 y último);

Tenga en cuenta que la siguiente definición no funcionará:

nulo find_last_elem (int * arr, int num_elem, int * last_elem);

Esto se debe a que *last_elem se está pasando en el último caso por valor. Esto significa que el puntero en sí no puede ser modificado por la función. Es por eso que necesitamos pasar la dirección del puntero en sí mismo para poder modificar el puntero desde dentro de la función. De manera similar, también se pueden imaginar indirecciones de orden superior (triple, cuádruple, etc.). Sin embargo, no son muy comunes.

C) La idea del puntero NULL .

El año pasado, me pidieron que realizara algunas entrevistas para posibles contrataciones de nuestro proyecto. Las personas que entrevisté tenían entre 3 y 6 años de experiencia en programación. Hubo una pregunta que hice a cada uno de ellos, pero solo un puñado de ellos pudo dar respuestas satisfactorias.

Lo que les pregunté fue qué sucede cuando compilamos y ejecutamos el siguiente código:

int * a = NULL;
* a = 5;

Esta es una pregunta con trampa. La respuesta aparente a esta pregunta es que el programa podría fallar y colapsar, lo que seguiría preguntando por qué sucedería esto. Sin embargo, muy pocos candidatos tuvieron una buena respuesta a esto. La razón real es que en la mayoría de las plataformas de software, mediante un contrato entre los compiladores y el sistema operativo, la página de memoria de dirección cero para cada proceso no tiene permisos de lectura / escritura por defecto. Esto se hace para proteger contra el puntero no inicializado. Si un puntero no se inicializa, podría apuntar a cualquier dirección en su programa, incluso aquellas de variables no relacionadas. Por lo tanto, podría terminar modificando accidentalmente variables de partes en su código que no tenía intención de modificar en absoluto. Al inicializar un puntero a NULL , básicamente se asegura a sí mismo que cualquier intento de desreferenciarlo antes de establecerlo en una dirección válida dará como resultado que su programa falle (en lugar de hacer cosas impredecibles).

El otro truco aquí es que la mayoría de los sistemas operativos proporcionan rutinas que permiten cambiar los permisos de las direcciones en tiempo de ejecución. Esto significa que un programa podría permitirse permisos de lectura / escritura en la dirección NULL . Al hacer esto, el programa ya no se bloqueará cuando el puntero NULL esté desreferenciado.

D) Cuerdas en C.

La mala literatura es la culpable de esto. El lenguaje C no tiene soporte inherente para las cadenas. Sin embargo, a la gente le gusta pensar que un puntero de caracteres y una cadena son lo mismo. Sin embargo, no lo son y un puntero de caracteres es simplemente un puntero a una variable de caracteres.

El hecho de que los punteros de caracteres a menudo se equiparen con cadenas se debe a que, hace mucho tiempo, las funciones de manipulación de cadenas que surgieron con esta convención ad hoc que para pasar una cadena a la función como parámetro, simplemente necesitará el puntero de caracteres para el carácter inicial en la cadena y toda la memoria que comienza desde este carácter hasta la primera aparición del carácter '\0' se considerará parte de la cadena. La convención se atascó y todas las funciones de manipulación de cadenas en y adhieren a esto. Sin embargo, la aplicación de esto proviene completamente de la implementación de dichas funciones y el compilador en sí mismo no hará nada para promoverlo.

En primer lugar, considérese afortunado, porque la mayoría de las personas encuentran el tema al menos un poco difícil, si no completamente desconcertante.

Creo que la gran razón de esto es que para la mayoría de las personas no es inmediatamente obvio qué hacen los punteros o para qué sirven. Si provienen de un lenguaje como Java o Python, se han llevado perfectamente bien sin ningún uso de punteros, así que, se preguntan, ¿C y C ++ “compran” para usted al introducir este nuevo concepto?

En contraste, para un programa inicial es bastante obvio lo que hace una declaración PRINT (de BASICA):

IMPRIMA “HOLA, SOY UNA COMPUTADORA”.

Tampoco es difícil entender el concepto de variables si alguna vez has estudiado incluso un poco de álgebra.

X = 1

Y = 2

IMPRIMA “ESTO ES X + Y:”, X + Y

Del mismo modo, el concepto de bucles y la toma de decisiones son directos para la mente humana, porque estas cosas corresponden a cosas que incluirías en las instrucciones para un humano. Por ejemplo, para encontrar un factorial de N:

  1. Comience configurando Respuesta igual a 1 e I igual a 1.
  2. Multiplicar la respuesta por I.
  3. Añadir 1 a I.
  4. Regrese al paso 2 hasta que sea mayor que N.

Mi punto es que muchas características de los lenguajes de programación corresponden a algo que podrías indicarle a un humano o robot que haga.

Pero, ¿cómo encajan los punteros en todo esto? Ellos no. Los punteros solo tienen sentido cuando comienzas a entender (y preocuparte) sobre la estructura interna de una computadora. Si no comprende eso fundamentalmente, las computadoras usan direcciones numéricas, y los nombres de variables son una especie de ilusión que en última instancia debe traducirse a esos números, no tiene mucho sentido.

Pero, básicamente, la mayoría de las personas se sentirán desconcertadas por los punteros a menos que los usen para realizar una tarea práctica, como producir el efecto de pasar por referencia.

SIN EMBARGO, hay un grupo de personas que encuentran los punteros extremadamente fáciles y directos. Esto consiste en personas que han usado lenguaje ensamblador o lenguaje máquina antes, porque usar dichos lenguajes debe verse obligado a comprender al menos un poco sobre la arquitectura de la computadora y el uso de direcciones.

Depende de cómo empezaste con las computadoras y cómo funciona tu mente.

Los punteros son fáciles. Comencé en el ensamblaje en aquellos días, porque era ensamblador o BÁSICO. No es que BASIC sea tan malo. Muchas personas nunca han escrito en ningún otro idioma. Y las variantes avanzadas no son tan malas como su nombre.

Pero es lento. Más un lenguaje de script que cualquier otra cosa.

Bueno. Los punteros para mí siempre fueron la representación mecánica de un objeto. Puede apuntar a cualquier estructura en memoria, bytes, enteros, cadenas, estructuras o funciones complejas. Lo único que debe hacer es realizar un seguimiento de lo que están señalando, si está en ensamblado.

Eso ni siquiera es un problema en “C”. El lenguaje lo rastrea. Y cuánto tiene que incrementar un puntero si desea el siguiente elemento en una matriz de ellos.

Para ser verdad, algunas de las otras estructuras HLL son mucho menos fáciles de entender. Estoy bastante seguro: si le preguntas a un programador decente de C ++ cuál es su “referencia” y cómo funcionará internamente, no podría darte una respuesta.

intercambio nulo (int & a, int & b) {
int t = a;
a = b;
b = t;
}
int main () {
int i = 1, j = 2;
intercambio (i, j);
}

Y eso es malo, es un problema real en HLL: las personas pueden usar fácilmente algo que no entienden.

Pregúntele a un programador de C lo mismo y él se lo explicará con entusiasmo.

E incluso si dice “No me importa cómo se hace, solo quiero que se intercambien”, puede que se pierda lo importante aquí: un ingeniero debe conocer su motor.

Pero las personas son flojas y tratan de evitar tener que saber sobre las cosas que hacen. En C lo mismo quisiera esto:

intercambio nulo (int * a, * b) {
int t = * a;
* a = * b;
* b = t;
}
int main () {
int i = 1, j = 2;
intercambio (& i, & j);
}

¿Todos ven lo que yo veo? Que la verdad sobre lo que está sucediendo aquí está oculta en C ++, pero obvia en C.

Es el primer ejemplo de “cuán maravilloso es C ++ y cuánto mejor que C”. Era el vendedor del sistema en aquellos días. Y solo dije: “¡Oye, espera! ¡Detener! ¡Eso es malo! La gente ya no ve lo que está haciendo. ¡Eso conducirá a errores realmente malvados y a esfuerzos de corrección de errores de muchas horas!

Estuvo mal. Sucedió exactamente lo que había visto en aquel entonces. Debido a que tal vez crecí con el ensamblaje, siempre me he enfrentado a las verdades detrás de nuestras máquinas. Y mi consejo es que lo hagas tú mismo.

Entonces sí. Los punteros son simples. Son fáciles de entender. Ellos son mejores.

Su código fuente puede verse mejor en C ++ pero oculta la realidad a sus ojos. Y entonces no puedes ver el error. Tienes que hacer muchas conjeturas.

En C, la única forma en que una persona que llama puede cambiar los parámetros es, si le da la dirección. En cualquier otro caso, no es posible que la persona que llama cambie su parámetro (¡si no está bloqueando la pila!). En C ++ esto puede suceder fácilmente y no se puede ver en main (). La persona que llama no puede verlo.

Eso es vago Eso es malo.

Entonces: profundizar en el puntero, es un buen concepto. Es inteligente, eficiente y literalmente le permite hacer cualquier cosa que una computadora pueda hacer. Y le permite programar en el estilo que elija. ¿Quieres programación funcional? Enviar punteros a funciones como un parámetro.

¿Quieres un objeto orientado?

Envíe punteros a estructuras de datos y oculte su código dentro de los módulos. C es un lenguaje de programación libre de paradigma. Puedes hacer cualquier cosa con eso.

Y los punteros son muy eficientes. Por lo general, se compilan en una sola línea de ensamblaje, es decir, un código de operación de procesador. Abrácelos. Amarlos.

El mejor concepto de todos.

Es una de esas cosas que el concepto no es tan difícil.

La complicación viene con la administración de punteros, pasándolos, traficando artefactos y miembros de clase punteros versus no punteros. (Y evitando pérdidas de memoria)

Los punteros tienen complicaciones, por ejemplo:

int x = 1;
int x = 2;

No es problema, el cambio X cambia los valores es todo.

int * x;
x = nuevo int;
* x = 52;
x = nuevo int; // Pérdida de memoria causada.

Si bien esto parece claro que esto está mal, solo estoy demostrando el concepto de una pérdida de memoria.
Ahora hablemos sobre la administración de código con 100 si no 1000 de clases, la mayoría de las cuales residen en la memoria de almacenamiento dinámico (usan punteros) y se crean dinámicamente y posiblemente se transmiten un poco.

Incluso puede tener funciones que crean nuevos objetos y los devuelven según los parámetros (por ejemplo, el patrón de diseño de fábrica).

Ahora debe tener en cuenta el alcance del objeto, y no solo lo “público” o “privado”, sino su ciclo de vida, dónde se crea, cómo se destruye.

Si intenta acceder a él después de la destrucción, obtendrá una infracción de acceso a la memoria (si tiene suerte) o problemas más grandes si no lo hace (afortunadamente, pocos casos le permitirían no obtenerla).

También puede tener un puntero a un objeto de ámbito local a un puntero

MyParent-> child.references-> object.attributes [….]. GetValue ();

Entonces, no es que un puntero sea una dirección, ese concepto es fácil. Es cuándo usar punteros, cuándo usar referencias, qué debe pasar y dónde, y lo más importante, cómo crear una aplicación sin pérdida de memoria.

El concepto de punteros, si se explican adecuadamente, es algo que la mayoría de la gente podría / puede entender.

Es en la práctica de usar punteros en software del mundo real que surgen complejidades.

Dependiendo de su experiencia como programador, también puede ser difícil comprender rápidamente el código de otras personas que contiene manipulación sofisticada de punteros.

Parece que, dado que afirma que acaba de comenzar a programar, todavía no tiene experiencia con punteros en un nivel no trivial. Espero sinceramente que a medida que encuentre un código más complicado, que los punteros sigan siendo fáciles de entender y manipular correctamente sin cometer errores. Sin embargo, será una minoría de programadores si este es el caso.

Eso es normal, los punteros son fáciles y absolutamente naturales si sabes cómo funciona una computadora. Nunca entendí el misterio a su alrededor. La sintaxis de C / C ++ puede ser complicada, pero es solo una señal de malos hábitos de codificación: al definir tipos con nombres descriptivos y usarlos en lugar de las muchas estrellas, el código es trivial y concebible para todos, al tiempo que genera un código idéntico a nivel de bits.

Hay más ideas falsas sobre los punteros nulos. El puntero nulo no es especial en el lenguaje C / C ++, solo es especial en la implementación estándar de la biblioteca. Esto puede ser interesante: la respuesta de Ferenc Valenta a ¿Qué sucede realmente al desreferenciar un puntero NULL? Por lo general, el proceso termina. ¿La reacción depende del sistema operativo o es controlada por el compilador? ¿Es obligatorio que NULL siempre se defina como “0” con la conversión adecuada?

Creo que tienes razón en tu analogía.

Creo que las personas encuentran difíciles los punteros cuando comienzan a aprender programación desde un nivel superior, donde el funcionamiento de la máquina se abstrae.

Mi título es EE en lugar de CS, y solo aprendí C (y, por lo tanto, punteros) porque se usó en una clase que quería tomar. En ese momento ya había tomado una clase de microprocesador donde programamos en lenguaje ensamblador y escribimos manejadores de interrupciones. Si sabe cómo escribir en ensamblador, los punteros son fáciles. Por supuesto, el ensamblaje de aprendizaje es difícil, por lo que no voy a afirmar que debe aprenderlo solo para facilitar los punteros.

Los punteros son básicamente una variable que almacena el valor de las variables de otra variable. Entonces es como si estuviera trabajando a través de una libreta de direcciones o un diccionario. Debe encontrar un índice y luego desplegarlo para clasificar el siguiente índice de su próximo índice. Los punteros son más fáciles de manejar cuando prediseña el flujo configurando su pseudo. Aquellos que encuentran complejo es porque no quieren gastar tiempo para sacar lo básico. No es complejo, es lo administrativo que eres.

Hay otras respuestas aquí que lo explican muy bien, pero dado que mi respuesta fue solicitada aquí es mi respuesta.

No estoy realmente seguro, porque los indicadores también me resultaron muy naturales y siempre me han parecido ridículamente fáciles de entender y trabajar.

Nunca le he preguntado a nadie que considere complejos los punteros por qué esto es así para ellos, y además de todos mis colegas que recuerdo no han tenido problemas con los punteros.

Solo puedo suponer que la programación en general debe ser difícil para las personas que no pueden comprender el uso de punteros. No todos están preparados para ser programadores, y en mi humilde opinión, empujamos a demasiadas personas a convertirse en programadores que no tienen la capacidad natural de ser un buen programador. Se trata de arrojar cuerpos calientes ante el problema de la escasez de programadores en el mundo. Es una pena, y odio tener que mantener su código heredado horriblemente malo, que he tenido que hacer más veces de las que puedo contar.

Sé que esta no es una gran respuesta, pero como se solicitó aquí, lo es. 🙂

Lamento decir que realmente es Dunning Kruger aquí. El concepto básico es simple. Usarlos de manera efectiva y sabia requiere experiencia, cuidado, habilidad y un poco de suerte. La gente siempre combina los punteros con la administración de la memoria, lo cual es fenomenalmente difícil de hacer correctamente en un sistema grande. Hay una razón por la que Java suplantó a C / C ++, y esa es la eliminación del puntero y la gestión manual de la memoria.

Los punteros básicos son bastante simples, sí.

La parte difícil viene cuando intentas hacer algo interesante con, por ejemplo, matrices de caracteres (cadenas C). Le garantizo que se perderá algo y pasará bastante tiempo buscando fallas de segmentación.

Los punteros en c pueden realizar muchas operaciones de sutura. Puede dejar que un puntero apunte a un puntero, lo que puede hacerlo mucho más complejo y crear casi todo. Por ejemplo, algunas estructuras de datos avanzadas como la lista vinculada y el árbol no se pueden implementar sin punteros.

Hay muchas cosas con punteros para aprender, sigue aprendiendo y encontrarás su encanto. No lo pienses tan simple.

Espero que esto ayude y buena suerte. //: ~

Felicidades, que es un gran paso para aprender a programar, no es inusual para algunas personas que usan C / C ++ durante muchos años, y aún así siempre tienen una pérdida de memoria para perseguir su código.

Me gustan los cuadros y las flechas si voy a dibujar un diagrama (bueno, a veces dibujo círculos 😉). De lo contrario, solo lo veo como números.

¿Ya ha dominado los conceptos de “pérdidas de memoria”, “punteros huérfanos” y “recolección de basura”?

Sí, el concepto de puntero parece fácil, pero lo que está diciendo es básicamente la definición básica de puntero. Cuando el puntero se usa para señalar una matriz o estructura o algunos códigos complejos, surge el problema.

Los punteros pueden parecer fáciles, pero de hecho necesitan pensar un poco. Debe tener cuidado al usar punteros, ya que utiliza directamente la manipulación de la memoria.

Aquí hay algunas desventajas de los punteros que lo hacen complejo:

  • Los punteros no inicializados pueden causar fallas de segmentación.
  • El bloque asignado dinámicamente debe liberarse explícitamente. De lo contrario, provocaría una pérdida de memoria.
  • Los punteros son más lentos que las variables normales.
  • Si los punteros se actualizan con valores incorrectos, puede provocar daños en la memoria.
  • Son difíciles de depurar.

Pero al final es importante si lo programa correctamente y si puede comprender el concepto de usar punteros mientras escribe código.