Comprender los punteros a punteros en C

0
15

Suena como una pesadilla de recursividad, pero los punteros a punteros son un elemento básico de la programación de sistemas. Básicamente, estás apuntando a una dirección de memoria que contiene la dirección de tus datos reales. Esta dirección indirecta de doble capa es lo que los desarrolladores llaman identificador. No es sólo un truco de codificación. Es un mecanismo necesario para que los sistemas operativos administren la memoria dinámica de manera eficiente.

Considere el montón como una habitación llena de gente. A veces, el sistema operativo necesita mover a las personas para hacer espacio. Si mantiene un puntero directo a alguien, no podrá moverse sin romper su referencia. Pero si mantiene presionado un puntero a una lista de nombres, el sistema operativo puede cambiar la ubicación de cualquier persona en esa lista sin que usted necesite actualizar su referencia inicial. Ese es el poder de un mango.

Así es como esa lógica se traduce en código C sin formato. Declaras p como puntero a un puntero. Entonces q se convierte en un puntero estándar. Usted asigna memoria para p, luego asigna memoria para lo que apunta p. Finalmente, desreferencias dos veces para asignar el valor 12.

Windows y macOS dependen de esta estructura para la compactación de la memoria. La distinción aquí es vital. Usted, el programador, gestiona el puntero externo p. El sistema operativo gestiona el puntero interno *p. Debido a que el sistema operativo controla *p, puede reubicar el bloque de datos real (**p) en cualquier parte del montón. Simplemente actualiza *p para reflejar la nueva dirección. Su código sigue usando p sin problemas.

Más allá de la gestión de la memoria del sistema operativo, este patrón es esencial para pasar punteros a funciones. Si necesita una función para modificar un puntero, pasa un puntero a ese puntero. Es la única forma de reasignar la referencia desde un ámbito diferente.

Gestión de punteros a estructuras

La complejidad no se limita a los números enteros simples. Puedes anidar esta lógica dentro de estructuras. Esto es común cuando se manejan datos de longitud variable como cadenas.

Tome la estructura Addr. Contiene matrices de tamaño fijo para nombres, ciudades y teléfonos. Pero los comentarios varían en longitud. Entonces “comentario” se define como un puntero a carácter. Cuando asigna memoria para la estructura misma, está reservando espacio para los punteros y las matrices fijas. Aún no estás reservando espacio para el texto del comentario real.

Primero asigna s. Luego lees la entrada del usuario en los buffers fijos. Después de eso, lees el comentario en un archivo temporal.

Pierdes memoria cuando ignoras cómo se anidan los punteros.

Tome un puntero s que apunte a una estructura. Esa estructura contiene otro indicador. Ese segundo puntero apunta a una cadena real en la memoria. Dos capas de indirección. Una asignación para la estructura. Una asignación para la cadena.

Bastante simple hasta que intentas limpiar.

Aquí es donde tropiezan la mayoría de los desarrolladores. Verás “gratis”. Crees que has terminado. Está usted equivocado.

Mire este fragmento de código:

La variable s apunta a la estructura Addr. Dentro de esa estructura, hay un campo “comentario”. “comentario” apunta a un bloque de memoria dinámica asignado para los datos de la cadena.

Cuando llamas “free(s)”, liberas la memoria para la estructura misma. La estructura Addr desapareció. La memoria se devuelve al sistema.

¿Pero qué pasa con s->comentario?

Desaparece en el éter.

El puntero a los datos de la cadena se almacenó dentro de la estructura que acaba de liberar. Ya no puedes acceder a él. No llamaste a free() en la cadena. La memoria permanece asignada pero inalcanzable.

Esta es una pérdida de memoria.

No es un choque. No es un mensaje de error. El programa funciona bien. Simplemente consume más RAM lentamente hasta que el sistema cambia o falla.

Cómo solucionar fugas de doble puntero

Primero debes liberar el puntero interior. O guárdelo en una variable temporal antes de liberar la estructura exterior.

Ahora se publican los datos de la cadena. Luego se libera la estructura. No hay bloques perdidos.

¿Por qué esto sucede tan a menudo?

La gente trata los punteros como valores. Olvidan que los indicadores son referencias a recursos.

Cuando una estructura contiene un puntero, esa estructura no es autónoma. Depende de la memoria externa. Liberar el contenedor no libera el contenido.

Esto empeora con un anidamiento más profundo. Un puntero a un puntero a un puntero. Tres niveles. Necesita tres llamadas gratis(). En el orden correcto.

Si olvidas uno, pierdes.

El problema de get()

El ejemplo utiliza obtiene(). Esa función es peligrosa. Se eliminó del estándar C11 porque permite desbordamientos del búfer. Pero la lógica de la memoria sigue siendo la misma.

Ya sea que use gets(), fgets() o scanf(), la estrategia de asignación es el cuello de botella de las fugas aquí.

Usted asigna para s.
Usted asigna para s->comentario.

Si solo libera s, deja s->comentario atrás.

Conclusiones clave

  • Los punteros dobles requieren doble gratis.
  • Verifique que cada malloc tenga un free correspondiente.
  • Si una estructura contiene un puntero a la memoria dinámica, esa memoria debe liberarse antes que la estructura.
  • free() no libera miembros de forma recursiva.

Es fácil pasarlo por alto. El código se compila. Funciona. La fuga guarda silencio.

Hasta que no lo sea.

Creación de listas enlazadas

Puede terminar con un desastre de memoria si se deshace del contenedor antes que los datos a los que apunta. Se limpia la estructura que sostiene el puntero. El bloque de cuerdas permanece. Se convierte en un bloque perdido. Esto sucede cuando el orden de eliminación es incorrecto.

Vinculación

Las estructuras pueden apuntar a sí mismas. Esto le permite encadenar registros idénticos. El resultado es una lista enlazada. Es una forma estándar de organizar datos en C.

Así es como lo defines:

estructura typedef { nombre char[21]; ciudad de carbón[21]; estado de carbón [21]; Dirección siguiente; } Dirección;
Dirección
primero;

El campo “siguiente” contiene la dirección del registro siguiente. Utiliza una variable de puntero único para iniciar la cadena.

El costo de la flexibilidad

El compilador le permite modificar reglas que pueden parecer contradictorias a primera vista. Con suficiente experiencia, puedes diseñar estructuras similares a la que se muestra arriba. Es un movimiento de poder.

Pero no está exento de riesgos. Estás caminando por una delgada línea entre el código inteligente y los espaguetis que no se pueden mantener.

Por qué es importante

No se trata sólo de sintaxis. Se trata de lo que el lenguaje te permite hacer cuando superas sus limitaciones.

  • Control : obtienes control granular sobre el diseño de la memoria.
  • Interoperabilidad : puedes hablar con bibliotecas C más fácilmente.
  • Rendimiento : A veces, pasar por alto los controles de seguridad ahorra ciclos.

¿El truco? El compilador no te salvará de ti mismo. Le permitirá compilar código que falla en tiempo de ejecución.

Cómo usarlo de forma segura

Si vas a hacer esto, hazlo con intención.

  1. Aíslelo : no extienda este patrón por todo su código base. Guárdelo en un módulo pequeño y bien probado.
  2. Documenta todo : El futuro agradecerás el presente. O odiarte. Probablemente te odio.
  3. Utilice abstracciones : envuelva los punteros sin formato o los bloques inseguros en una interfaz limpia. Oculta el desorden.

La verificación de la realidad

La mayoría de los desarrolladores no necesitan esto. Utilizarán estructuras estándar. Estarán más felices por ello.

Pero cuando te topes con una pared donde la biblioteca estándar no encaja, te alegrarás de que el compilador no te haya detenido.

La pregunta es si está dispuesto a pagar el coste de mantenimiento.

Es una compensación. Velocidad por seguridad. Poder para la claridad.

Tú eliges.

попередня статтяUnix vs Linux: Por qué la distinción es importante para los desarrolladores
наступна статтяCómo los sintetizadores cambiaron la música rompiendo las reglas de la forma