Programación En C: Punteros

Este artículo es parte de una serie – Cómo programar cualquier cosa: C Programación

Prefacio

Hasta ahora, hemos cubierto tipos de variables básicos y su extensión, la matriz en la Programación previa en publicaciones C. Esta publicación se centra en el tercer componente principal cuando se trata de variables, valores y memoria: el puntero. Un puntero es básicamente una variable que contiene la dirección de memoria de un dato. Como C es un lenguaje de nivel medio, todavía tiene restos de lenguaje ensamblador en su funcionamiento. Lo que quiero decir con eso es que en lenguajes de nivel superior como PHP o Python, rara vez o nunca tendrás que lidiar con las direcciones de memoria específicas de cualquier variable, pero en C es un lugar común. Los apuntadores son necesarios en C para admitir la asignación de memoria dinámica, para unir piezas de datos dispares, como en una lista enlazada, para pasar las estructuras de datos originales a funciones que luego pueden modificarlas, y demás.

Desafortunadamente, los indicadores pueden ser un poco confusos y aún más desafortunados, pueden dar lugar a errores extraños. Esto se debe a que son extremadamente poderosos en su capacidad para señalar cualquier dirección en la memoria de toda la computadora. Use un puntero que no haya sido inicializado a algo dentro de su propio programa y puede estar sobrescribiendo su propio código, la pila, incluso posiblemente el espacio de memoria de su sistema operativo.

Los punteros utilizados correctamente son formas muy prácticas de abordar la memoria y ocuparse de bloques de espacio contiguos, como matrices. Sin punteros, no sería capaz de asignar bloques dinámicos de memoria en su montón. Los punteros incluso pueden apuntar a funciones y le permiten pasar funciones a otras funciones, en efecto, desconectando y conectando diferentes funcionalidades. A este respecto, C tiene propiedades de programación funcional limitadas.

Qué Es Un Puntero

Un puntero es una variable que, en lugar de almacenar un valor directo del programador, almacena una dirección de memoria. Esta dirección es la ubicación de cualquier cantidad de cosas, incluidos bloques de memoria, funciones y otras variables. Cuando una variable contiene la dirección de otra variable, se dice que la primera variable apunta a la segunda variable, o función, o lo que tienes.
El diagrama de arriba ilustra de lo que estoy hablando. La variable ubicada en la dirección de memoria 0x005 contiene el valor 11. Sin embargo, la variable ubicada en la dirección de memoria 0x008 contiene la dirección de memoria de la primera variable. La flecha muestra cómo esa dirección de memoria, cuando se resuelve, apunta al valor almacenado en 0x005. A este respecto, la variable almacenada en 0x008 es un puntero y apunta al valor ubicado en 0x005.

Cómo Definir Un Puntero

Como en todo el resto de C, para que una variable actúe como un puntero, debe definirse como un puntero por adelantado. Hacer esto requiere una pequeña extensión a la declaración de variable básica que cubrimos en Tipos de datos básicos. Cuando hay una declaración de tipo que pretende ser un puntero, coloca un asterisco después de la especificación de tipo. Entonces, si estoy abajo, por ejemplo, defino una variable de tipo char, y luego procedo a definir una variable de puntero de tipo char *:

NOTA: De aquí en adelante cuando escribo comentarios en fragmentos de código, a veces uso los comentarios C89 (comenzando con / * y terminando con * /) pero, en general, para comentarios más pequeños, usaré el sistema de comentarios de una sola línea C99 (cualquier cosa que empiece por un //)

Es importante tener en cuenta que una variable de puntero, aunque técnicamente puede señalar cualquier dirección en la memoria, tiene un tipo de base definido en su declaración. No solo define un puntero, define un puntero int o un puntero char. Esto es muy importante cuando se trata del comportamiento del puntero más adelante en su programación. Esto se debe a que todas las operaciones del puntero, como veremos pronto, son relativas al tipo del puntero.

Existe tal cosa como un puntero de vacío. Esto también se conoce como un puntero genérico. Este puntero es un puntero de propósito general y, aunque poderoso, está algo limitado en operaciones de puntero. Está destinado a apuntar a cualquier cosa, muchas veces sosteniendo temporalmente una dirección en la memoria hasta que pueda convertirse en un puntero de un tipo más específico.

Sí, un tipo de puntero se puede convertir en otro tipo de puntero. Esto se conoce como conversión de puntero. Esto es lo primero y más importante donde los tipos de indicadores se hacen evidentes. Si lanza un puntero doble a un puntero int, y luego usa ese puntero int para asignar un valor, no obtendrá un doble, solo obtendrá los primeros cuatro bytes: un int. Examinaremos esto más de cerca en la asignación del puntero.

Los Operadores De Punteros

No hemos discutido los operadores hasta ahora, pero en pocas palabras los operadores son básicamente declaraciones / símbolos que realizan acciones sobre cualquier variable / dato dado. Por ejemplo, en el enunciado 2 + 5, + es el operador de suma y toma dos operandos, resuelve / devuelve el valor de 7. Para los indicadores hay dos operadores unarios (lo que significa que solo requiere un operando) que nos permiten trabajar con direcciones de memoria. Estos son * y &.

El & Operador

El operador & es bastante fácil de entender. Es un operador unario que toma cualquier expresión / variable que viene después y devuelve su dirección de memoria. Personalmente, leí al operador & como “la dirección de” cualquier variable que aborde en este momento. Es importante poder resolver una variable en su dirección de memoria para que podamos asignar esa dirección a un puntero. Examine el siguiente código:

Si no hubiéramos usado el operador & en la última declaración, estaríamos asignando el valor 7 al puntero. Esto haría que el puntero apuntara a la dirección de memoria 7, y si tuviéramos que usarla más tarde, obtendríamos un error importante. Tal como está, con el operador &, myIntegerPionter ahora contiene la dirección de memoria de myIntegerVariable.

El * Operador

El operador * es un poco más difícil de entender, en mi opinión. Una razón es porque comparte el mismo símbolo con las declaraciones de tipo de puntero, y cuando se mezcla el operador * con las declaraciones de tipo de puntero (tipo *) puede ser un poco extraño. La otra razón es porque puede usar el operador * en una variable dentro de una declaración de asignación, que me resulta impar. Yo lo explicaré.

El operador * es el opuesto conceptual del operador & en que, en lugar de devolver una dirección de memoria, en su lugar devuelve el valor que está almacenado en la dirección de memoria mantenida por el puntero. Es un operador único al igual que el operador &. Para ampliar nuestro ejemplo, podríamos escribir algo como esto después del ejemplo anterior:

En este ejemplo, anotherIntegerVariable ahora contiene el valor originalmente almacenado en myIntegerVariable arriba. Me gusta leer el * operador como diciendo “en la dirección de”. Aunque, para ser sincero, eso es un poco torpe para mí, y en su lugar realmente lo imagino como simplemente un operador de resolución. Resuelve cualquier dirección de memoria que se esté almacenando y accede a esa dirección de memoria. La razón por la que digo esto es porque este tipo de código es posible:

¿Viste esa última línea? En esa línea asigné el número 7 a la variable almacenada en la dirección de memoria de myPtr, que en este caso es myVar; Al final de la ejecución del bloque anterior myVar es igual a 7. Es por eso que me imagino algo así como   *myPtr  para indicar que el compilador está resolviendo esa expresión en la dirección de memoria almacenada, en este caso, es equivalente a myVar.

Matrices de Punteros

Los punteros también se pueden definir en matrices. Puede indexarlos tal como lo haría con los valores normales. Por ejemplo, puede crear una matriz de diez elementos con esta fácil declaración. También muestro ejemplos de asignación a un elemento particular en la matriz, así como la resolución de un elemento en la matriz.

Tenga en cuenta que esto es diferente de lo que se conoce como matrices asignadas dinámicamente, que se tratarán en otro artículo.

Punteros a Punteros

En realidad, es posible tener un puntero a otro puntero. Esto se conoce como indirección múltiple. Es más útil cuando se trata de varios sistemas de memoria y sus problemas correspondientes. Un puntero a un puntero no es diferente de un puntero a un objeto normal en términos de almacenamiento y resolución de direcciones de memoria. Sin embargo, su declaración es diferente desde el punto de vista del compilador:

Tenga en cuenta los dos asteriscos. Estos indican que este es un puntero a … otro puntero. Este fragmento de código puede ilustrar la idea aquí mejor de lo que puedo en un párrafo:

Como puede ver, debemos asignar myMultipleIndirection, definir como (char **), a la dirección de un puntero. Esto se debe a que es un puntero que apunta a un puntero. Este tipo de indirección es un poco esotérico, pero se usa a menudo. Solo recuerda que los punteros son variables en sí mismos y también pueden tener sus propias direcciones de memoria.

Asignación de Puntero (Conversión)

Ha visto la asignación básica de punteros en los ejemplos dados para los operadores de puntero anteriores. Si desea asignar un puntero a la dirección de memoria de algo, use el operador &. Si desea que un puntero contenga la misma dirección de memoria que otro puntero (en efecto, apunte a lo mismo) simplemente asigne un puntero a otro sin ningún operador adicional:

¿Qué sucede si tiene diferentes tipos de punteros pero quiere que apunten a la misma cosa? Aquí es donde entra la conversión del puntero. C tiene un método llamado “fundición explícita”, donde puede forzar a una variable / puntero a simular que es de otro tipo para que el compilador no se queje de que los tipos no coinciden. Para hacer casting, coloca el tipo entre paréntesis antes de la expresión. Así que asignemos el puntero char a un valor int:

Este es un código valido Sin embargo, debe recordar, como se indicó anteriormente, que un puntero de char siempre actuará como un char. Solo hará referencia a un byte. Si tuviéramos que asignar otra variable resolviendo myPtr, solo obtendrás el primer byte del entero myVariable, ya que los caracteres solo almacenan un byte, mientras que un int largo almacena cuatro.

Aritmética del Puntero

Ah sí, la temida aritmética del puntero. No es tan complicado como la palabra en la calle lo ha hecho sonar. Solo tiene que recordar que todas las operaciones se realizan en relación con el tipo de base del puntero dado. Primero, definamos las restricciones. Solo puede realizar dos operaciones en punteros, y esas son sumas y restas. De hecho, solo puede agregar enteros a punteros, no a valores flotantes o dobles. No puede agregar dos punteros juntos, aunque puede restarlos (llegaremos a eso).

¿Qué sucede cuando incrementa o disminuye un puntero? Depende completamente del tipo de base del puntero. Sin embargo, la regla es que el puntero apunta a la siguiente dirección de memoria que podría contener el tipo de base. Entonces, por ejemplo, si incrementamos un puntero de char, obtendremos el siguiente byte relativo a su dirección de memoria contenida. Esto es porque un char ocupa un byte. Sin embargo, si incrementamos un puntero int largo, saltaremos cuatro bytes relativos a su dirección de memoria contenida. Y esto se debe a que int largo ocupa cuatro bytes. El siguiente diagrama muestra esto gráficamente:

Cada vez que agregamos 1 al puntero (largo int *) avanzamos cuatro bytes, mientras que cada vez que agregamos 1 al puntero (char *) avanzamos un byte. Esto se debe a que el puntero avanza según su tamaño de tipo de base. Si el tipo de base de un puntero ocupaba 23 bytes, cada vez que avanzáramos el puntero en 1 señalaría 23 bytes más adelante en la memoria. La aritmética del puntero ocurre en relación con el tamaño del tipo de base del puntero.

Puede restar un puntero de otro puntero del mismo tipo base. Esto le dará la cantidad de objetos de tipo base entre los dos. Es decir, si resta uno (int largo *) de otro puntero (largo int *), obtendrá el número de enteros largos que existen entre las dos direcciones de memoria.

Comparación de Punteros

Es posible comparar un puntero a otro puntero. El puntero que apunta a la dirección de memoria inferior aparecerá como más pequeño. Esto es útil cuando se trata de punteros y matrices, o punteros que apuntan al mismo objeto o variable. Cualquiera de los varios operadores de comparación (>, <,> =, <=) se puede usar con respecto a los punteros.

Conclusión

Los indicadores en este punto de la serie de artículos pueden parecer más un dolor que una bendición, pero son muy poderosos a la hora de construir programas articulados. Los punteros nos permiten pasar la dirección de los objetos en la memoria a las funciones, y nos permiten apuntar a la memoria asignada dinámicamente. Los punteros y sus relaciones con las matrices, así como su relación con la asignación de memoria dinámica es el tema de los artículos que se incluirán en la serie. Por ahora, los indicadores para nosotros simplemente apuntan a una posición en la memoria. Como resumen, recuerde que el operador & nos da la dirección de memoria de una variable, y el operador * resuelve una dirección de memoria almacenada. Con estos dos elementos en mente, los indicadores no tienen por qué ser intimidantes, y de hecho pueden ser divertidos. Espero haberlo ayudado a aprender algo de este tutorial, ¡gracias por leer!

Este artículo es parte de una serie – Cómo programar cualquier cosa: Programación C

Si usted aprecia este artículo usted puede ser que considere el apoyar de mi Patreon.

Pero si un compromiso mensual es un poco más, lo entiendo, podría considerar comprarme un café.

photo credit: bmnnetwork isac-youtube via photopin (license)

También te podría gustar...

Deja un comentario

A %d blogueros les gusta esto: