














Prepara tus exámenes y mejora tus resultados gracias a la gran cantidad de recursos disponibles en Docsity
Gana puntos ayudando a otros estudiantes o consíguelos activando un Plan Premium
Prepara tus exámenes
Prepara tus exámenes y mejora tus resultados gracias a la gran cantidad de recursos disponibles en Docsity
Prepara tus exámenes con los documentos que comparten otros estudiantes como tú en Docsity
Los mejores documentos en venta realizados por estudiantes que han terminado sus estudios
Estudia con lecciones y exámenes resueltos basados en los programas académicos de las mejores universidades
Responde a preguntas de exámenes reales y pon a prueba tu preparación
Consigue puntos base para descargar
Gana puntos ayudando a otros estudiantes o consíguelos activando un Plan Premium
Comunidad
Pide ayuda a la comunidad y resuelve tus dudas de estudio
Descubre las mejores universidades de tu país según los usuarios de Docsity
Ebooks gratuitos
Descarga nuestras guías gratuitas sobre técnicas de estudio, métodos para controlar la ansiedad y consejos para la tesis preparadas por los tutores de Docsity
Este documento proporciona una introducción a los punteros en el lenguaje de programación c. Se explican conceptos fundamentales como la declaración de punteros, la desreferenciación, la aritmética de punteros y el manejo de arreglos con punteros. Se incluyen ejemplos prácticos para ilustrar el uso de punteros en diferentes escenarios.
Tipo: Apuntes
1 / 22
Esta página no es visible en la vista previa
¡No te pierdas las partes importantes!
2.1 Apuntadores (punteros) Una sólida comprensión de los apuntadores y la capacidad de usarlos de manera efectiva separa a un programador de C novato de uno más experimentado. Los apuntadores impregnan el lenguaje y proporcionan gran parte de su flexibilidad. Proporcionan un soporte importante para la asignación de memoria dinámica, están estrechamente vinculados a la notación de matriz y, cuando se usan para señalar funciones, agregan otra dimensión al control de flujo en un programa. Los apuntadores han sido durante mucho tiempo un obstáculo en el aprendizaje de C. El concepto básico de un puntero es simple: es una variable que almacena la dirección de una ubicación de memoria. El concepto, sin embargo, se complica rápidamente cuando comenzamos a aplicar operadores de puntero y tratamos de discernir sus anotaciones a menudo crípticas. Pero esto no tiene por qué ser así. Si comenzamos de manera simple y establecemos una base firme, entonces los usos avanzados de los punteros no son difíciles de seguir y aplicar. La clave para comprender los punteros es comprender cómo se gestiona la memoria en un programa de lenguaje C. Después de todo, los punteros tienen direcciones en la memoria. Si no entiende cómo se organiza y gestiona la memoria, es difícil entender cómo funcionan los punteros. Para abordar esta preocupación, la organización de la memoria se ilustra siempre que sea útil para explicar un concepto de puntero. Una vez que tenga una comprensión firme de la memoria y las formas en que se puede organizar, comprender los punteros se vuelve mucho más fácil. 2.1.1 Apuntadores y direcciones de memoria Cuando se compila un programa en C, trabaja con tres tipos de memoria: Estático / Global Las variables declaradas estáticamente se asignan a este tipo de memoria. Las variables globales también usan esta región de memoria. Se asignan cuando se inicia el programa y siguen existiendo hasta que finaliza el programa. Si bien todas las funciones tienen acceso a variables globales, el alcance de las variables estáticas está restringido a su función de definición. Automático Estas variables se declaran dentro de una función y se crean cuando se llama a una función. Su alcance está restringido a la función y su tiempo de vida está limitado al tiempo que se ejecuta la función. Dinámica La memoria se asigna desde el montón (heap en inglés) y se puede liberar según sea necesario. Un puntero hace referencia a la memoria asignada. El alcance se limita al puntero o punteros que hacen referencia a la memoria. Existe hasta que se libera.
En la tabla 2.1 se muestra el alcance y la vida útil de las variables utilizadas en las regiones indicadas anteriormente Tabla 2.1 Alcance y vida de una variable Variable Alcance Tiempo de vida en el programa Global Todo el código Todo el tiempo de ejecución Automático (local) La función en la que se declara Mientras se ejecuta la función Dinámica Determinada por los punteros que hacen referencia a la memoria Hasta que se libera la memoria Comprender estos tipos de memoria le permitirá comprender mejor cómo funcionan los punteros. La mayoría de los punteros se utilizan para manipular datos en la memoria. Comprender cómo se divide y organiza la memoria aclarará cómo los punteros manipulan la memoria. Una variable de puntero contiene la dirección en la memoria de otra variable, objeto o función. Se considera que un objeto está asignado a la memoria utilizando una de las funciones de asignación de memoria, como la función malloc(). Normalmente se declara que un puntero es de un tipo específico dependiendo de a qué apunta, el objeto puede ser cualquier tipo de datos del lenguaje C, como un número entero, un carácter, una cadena o una estructura. Sin embargo, nada inherente a un puntero indica a qué tipo de datos hace referencia el puntero. Un puntero solo contiene una dirección. Los punteros tienen varios usos, entre ellos: Creación de código rápido y eficiente Proporcionar un medio conveniente para abordar muchos tipos de problemas Compatibilidad con la asignación de memoria dinámica Hacer expresiones compactas y sucintas Proporcionar la capacidad de pasar estructuras de datos por puntero sin incurrir en una gran sobrecarga Protección de datos pasados como parámetro a una función Se puede escribir un código más rápido y eficiente porque los punteros están más cerca del hardware. Es decir, el compilador puede traducir más fácilmente la operación a código de máquina. No hay tanta sobrecarga asociada con los punteros como podría estar presente con otros operadores. Muchas estructuras de datos se implementan más fácil empleando punteros. Por ejemplo, una lista enlazada podría admitirse mediante matrices o punteros. Sin embargo, los punteros son más fáciles de usar y se asignan directamente a un enlace anterior o siguiente. Una implementación de matriz requiere índices de matriz que no son tan intuitivos o flexibles como los punteros. La asignación de memoria dinámica se efectúa en el lenguaje C mediante el uso de punteros. Las funciones malloc() y free() se utilizan para asignar y liberar memoria dinámica, respectivamente. La asignación de memoria dinámica permite matrices y estructuras de datos de tamaño variable, como listas y colas vinculadas. Sin embargo, en el nuevo estándar C, C11, se admiten matrices de tamaño variable. Las expresiones compactas pueden ser muy descriptivas, pero también pueden ser crípticas, ya que muchos programadores no siempre entienden completamente la notación de punteros. Las expresiones compactas deben abordar una necesidad específica y no ser crípticas solo por ser crípticas. Por ejemplo, en la siguiente secuencia de instrucciones char *nombres[] = { “Pablo”, “Maria”, “Alberto” }; printf(“%c\n”, ((nombres+1)+2)); printf(“%c\n”, nombres[1][2]));
El uso de espacios en blanco es una cuestión de preferencia del usuario. El asterisco declara la variable como un puntero. Es un operador (símbolo) sobrecargado (tiene varios usos) ya que también se usa para multiplicar y des referenciar un puntero. La figura 2.1 ilustra cómo se asignaría normalmente la memoria para la declaración anterior. Los tres rectángulos representan tres ubicaciones de memoria. El número a la izquierda de cada rectángulo es su dirección. El nombre junto a la dirección es la variable asignada a esta ubicación. La dirección 100 se utiliza aquí con fines ilustrativos. La dirección real de un puntero, o cualquier variable, normalmente no se conoce, ni su valor es de interés en la mayoría de las aplicaciones. Los tres puntos representan la memoria no inicializada. Figura 2.1 Memoria asignada a número y ptr. Los punteros a la memoria no inicializada pueden ser un problema. Si se elimina la referencia a dicho puntero, es probable que el contenido del puntero no represente una dirección válida y, si lo hace, es posible que no contenga datos válidos. Una dirección no válida es aquella a la que el programa no está autorizado a acceder. Esto dará como resultado que el programa finalice en la mayoría de las plataformas, lo cual es significativo y puede generar una serie de problemas, las variables numero y ptr están ubicados en las direcciones 100 y 104, respectivamente. Se supone que ambos ocupan cuatro bytes. Ambos tamaños diferirán, dependiendo de la configuración del sistema como se verá en la sección “Tamaño y tipos de puntero”. A menos que se indique lo contrario, utilizaremos números enteros de cuatro bytes para todos nuestros ejemplos. Puntos a recordar: El contenido de la variable ptr eventualmente se le debe asignar la dirección de una variable entera. Estas variables no se han inicializado y, por lo tanto, contienen basura. No hay nada inherente a la implementación de un puntero que sugiera a qué tipo de datos hace referencia o si su contenido es válido. Sin embargo, el tipo de puntero se ha especificado y el compilador se quejará con frecuencia cuando el puntero no se utilice correctamente. Si bien un puntero se puede usar sin inicializarlo, es posible que no siempre funcione correctamente hasta que se haya inicializado. Cómo leer una declaración Ahora es un buen momento para sugerir una forma de leer las declaraciones de puntero, lo que puede hacerlas más fáciles de entender. El truco es leerlos al revés. Si bien aún no hemos discutido los punteros a las constantes, examinemos la siguiente declaración: const int *ptr; Leer la declaración hacia atrás nos permite entender progresivamente la declaración:
Los punteros a void se explican en “Anular Puntero”. Para mantener nuestros ejemplos simples, usaremos el especificador %p y no enviar la dirección a un puntero para anular. Memoria virtual y punteros Para complicar aún más la visualización de direcciones, las direcciones de puntero que se muestran en un sistema operativo virtual no es probable que sean las direcciones de memoria física reales. Un sistema operativo virtual permite dividir un programa en el espacio de direcciones físicas de la máquina. Una aplicación se divide en páginas/marcos. Estas páginas representan áreas de la memoria principal. Las páginas de la aplicación se asignan a diferentes áreas de memoria potencialmente no contiguas y es posible que no todas estén en la memoria al mismo tiempo. Si el sistema operativo necesita la memoria que actualmente tiene una página, la memoria puede cambiarse a un almacenamiento secundario y luego volver a cargarse en un momento posterior, con frecuencia en una ubicación de memoria diferente. Estas capacidades proporcionan un sistema operativo virtual con una flexibilidad considerable en la forma en que administra la memoria. Cada programa asume que tiene acceso a todo el espacio de memoria física de la máquina. En realidad, no es así. La dirección utilizada por un programa es una dirección virtual. El sistema operativo asigna la dirección virtual a una dirección de memoria física real cuando es necesario. Esto significa que el código y los datos en una página pueden estar en diferentes ubicaciones físicas a medida que se ejecuta el programa. Las direcciones virtuales de la aplicación no cambian; son las direcciones que vemos cuando examinamos el contenido de un puntero. El sistema operativo asigna de forma transparente las direcciones virtuales a las direcciones reales. El sistema operativo maneja todo esto, y no es algo sobre lo que el programador tenga control o deba preocuparse. Comprender estos problemas explica las direcciones devueltas por un programa que se ejecuta en un sistema operativo virtual. Des referenciar un puntero usando el operador de indirección El operador de direccionamiento indirecto * (indirección), devuelve el valor al que apunta una variable de puntero. Esto se conoce con frecuencia como des referenciar un puntero. En el siguiente ejemplo, numero y ptr se declaran e inicializan: int numero = 5; int *ptr = № El operador de direccionamiento indirecto se usa en la siguiente declaración para mostrar 5, el valor de numero: printf("%p\n", *ptr); // Muestra 5 También podemos usar el resultado del operador de desreferencia como valor. El valor del término se refiere al operando que se encuentra en el lado izquierdo del operador de asignación. Todos los valores deben ser modificables ya que se les está asignando un valor. Lo siguiente asignará 200 al entero señalado por ptr. Como apunta a la variable número, 200 será asignado a número. La figura 2.3 ilustra cómo se ve afectada la memoria: *ptr = 200; printf("%d\n", numero); // Muestra 200
Figura 2.3 Memoria asignada usando el operador de desreferencia (indirección) Punteros a funciones Se puede declarar un puntero para que apunte a una función. La notación de declaración es un poco críptica. A continuación, se ilustra cómo declarar un puntero a una función. La función se pasa void y devuelve void. El nombre del puntero es foo: void (*foo)(); Apuntador a una función es un área temática rica y se tratará con más adelante. El concepto de nulo El concepto de nulo es interesante y, a veces, se malinterpreta. La confusión puede ocurrir porque a menudo tratamos con varios conceptos similares pero distintos, que incluyen: El concepto nulo La constante de puntero nulo El macro NULL El ASCII NUL Una cadena nula La sentencia nula Cuando NULL está asignado a un puntero, significa que el puntero no apunta a nada. El concepto nulo se refiere a la idea de que un puntero puede contener un valor especial que no es igual a otro puntero. No apunta a ninguna área de la memoria. Dos punteros nulos siempre serán iguales entre sí. Puede haber un tipo de puntero nulo para cada tipo de puntero, como un puntero a un carácter o un puntero a un número entero, aunque esto es poco común. El concepto nulo es una abstracción respaldada por la constante de puntero nulo. Esta constante puede o no ser un cero constante. El programador del lenguaje C no necesita preocuparse por su representación interna real. La macro NULL es una conversión de cero enteros constantes a un puntero para anular. En muchas bibliotecas, se define de la siguiente manera: #define NULL (( void *)0) Esto es lo que normalmente consideramos un puntero nulo. Su definición se puede encontrar con frecuencia dentro de varios archivos de encabezado diferentes, incluidos stddef.h, stdlib.h, y stdio.h. Si el compilador utiliza un patrón de bits distinto de cero para representar un valor nulo, es responsabilidad del compilador asegurarse de que todos los usos de NULL o 0 en un contexto de puntero se tratan como punteros nulos. La representación interna real de nulo está definida por la implementación. El uso de NULL y 0 son símbolos de nivel de idioma que representan un puntero nulo. El ASCII NUL se define como un byte que contiene todos ceros. Sin embargo, esto no es lo mismo que un puntero nulo. Una cadena en lenguaje C se representa como una secuencia de caracteres terminada en un valor cero. La cadena nula es una cadena vacía y no contiene ningún carácter. Finalmente, la declaración nula consta
ptr = № *ptr = 0; // Cero se refiere al entero cero Estamos acostumbrados a operadores sobrecargados, como el asterisco que se usa para declarar un puntero, para des referenciar un puntero o para multiplicar. El cero también está sobrecargado. Puede que esto nos resulte incómodo porque no estamos acostumbrados a sobrecargar operandos. Puntero a void Un puntero a void es un puntero de uso general que se utiliza para contener referencias a cualquier tipo de datos. A continuación, se muestra un ejemplo de un puntero a void: void *ptrv; Tiene dos propiedades interesantes: Un puntero a void tendrá la misma representación y alineación de memoria que un puntero a char. Un puntero a void nunca será igual a otro puntero. Sin embargo, dos punteros vacíos asignaron un NULL el valor será igual. Cualquier puntero se puede asignar a un puntero para anular. A continuación, se puede convertir de nuevo a su tipo de puntero original. Cuando esto suceda, el valor será igual al valor del puntero original. Esto se ilustra en la siguiente secuencia, donde un puntero a int se asigna a un puntero para anular y luego de vuelta a un puntero a int : int numero; int *ptr = № printf("Valor de ptr: %p\n", ptr); void * ptrv = ptr; ptr = ( int *) ptrv; ptintf("Valor de ptr: %p\n", ptr); Cuando esta secuencia se ejecuta como se muestra a continuación, la dirección del puntero es la misma: Valor de ptr: 100 Valor de ptr: 100 Los punteros a void se utilizan para punteros de datos, no para punteros de función. En “Polimorfismo en lenguaje C”, se usa los punteros a void para abordar el comportamiento polimórfico. Tenga cuidado al usar punteros a NULL. Si lanza un puntero arbitrario a un puntero para anular, no hay nada que le impida lanzarlo a un tipo de puntero diferente. El operador sizeof se puede utilizar con un puntero a NULL. Sin embargo, no podemos usar el operador con void como se muestra a continuación: size_t tamanio = sizeof( void *); // Legal size_t tamanio = sizeof( void ); // Ilegal El tipo size_t es un tipo de datos utilizado para tamaños y se analiza en “Tipos relacionados con punteros predefinidos”.
Punteros globales y estáticos Si un puntero se declara como global o estático, se inicializa a NULL cuando se inicia el programa. A continuación, se muestra un ejemplo de un puntero global y estático: int * globalPtr; void foo() { static int * estaticoPtr;
... } int main( int argc, char *argv[]) { ... return 0; } La figura 2.4 ilustra este diseño de memoria. Los marcos de pila se insertan en la pila y el montón se usa para la asignación de memoria dinámica. La región sobre el montón se usa para variables estáticas/globales. Este es un diagrama conceptual solamente. Las variables estáticas y globales se colocan con frecuencia en un segmento de datos separado del segmento de datos utilizado por la pila y el montón. Figura 2.4 Asignación de memoria para punteros globales y estáticos Tamaño y tipos de puntero El tamaño del puntero es un problema cuando nos preocupamos por la compatibilidad y la portabilidad de las aplicaciones. En la mayoría de las plataformas modernas, el tamaño de un puntero a datos normalmente es el mismo independientemente del tipo de puntero. Un puntero a un tipo se tiene el mismo tamaño que un puntero a una estructura. Si bien el estándar C no dicta que el tamaño sea el mismo para todos los tipos de datos, este suele ser el caso. Sin embargo, el tamaño de un puntero a una función puede ser diferente del tamaño de un puntero a datos. El tamaño de un puntero depende de la máquina en uso y del compilador. Por ejemplo, en las versiones modernas de Windows, el puntero tiene una longitud de 32 o 64 bits. Para los sistemas operativos DOS y Windows 3.1, los punteros tenían una longitud de 16 o 32 bits. Modelos de memoria La introducción de máquinas de 64 bits ha hecho más evidentes las diferencias en el tamaño de la memoria asignada para los tipos de datos. Con diferentes máquinas y compiladores vienen diferentes opciones para asignar espacio a los tipos de datos primitivos de C. Una notación común utilizada para describir diferentes modelos de datos se resume a continuación: I In L Ln LL LLn P Pn
obtendrá resultados poco confiables. El especificador de formato recomendado es %zu. Sin embargo, esto no siempre está disponible. Como alternativa, considere usar %tu o %lu. Considere el siguiente ejemplo, donde definimos una variable con size_t y luego mostrarlo usando dos especificadores en formato diferentes: size_t tamaniot = - 5; printf("%d\n", tamaniot); printf("%zu\n", tamaniot); Dado que una variable de tipo size_t está diseñada para usarse con números enteros positivos, usar un valor negativo puede presentar problemas. Cuando le asignamos un número negativo y usamos los especificadores de formato %d y luego el %zu, obtenemos el siguiente resultado:
Usando intptr_t y uintptr_t Los tipos intptr_t y uintptr_t se utilizan para almacenar direcciones de puntero. Proporcionan una forma portátil y segura de declarar punteros y tendrán el mismo tamaño que el puntero subyacente utilizado en el sistema. Son útiles para convertir punteros a su representación entera. El tipo uintptr_t es la versión sin firmar de intptr_t. Para la mayoría de las operaciones intptr_t se prefiere. El tipo uintptr_t no es tan flexible como intptr_t. A continuación, se ilustra cómo utilizar intptr_t: int numero; intptr_t * ptr = № Si tratamos de asignar la dirección de un número entero a un puntero de tipo uintptr_t de la siguiente manera, obtendremos un error de sintaxis: uintptr_t * ptrU = № El error sigue: error: invalid conversion from 'int' to 'uintptr_t {aka unsigned int}' [-fpermissive] Sin embargo, realizar la tarea con una mutación (cast) y eso funcionará: intptr_t ptr = № uintptr_t ptrU = (uintptr_t)№ no podemos usar uintptr_t con otros tipos de datos sin conversión: char c; uintptr_t * ptrC = (uintptr_t) &c; Estos tipos deben usarse cuando la portabilidad y la seguridad son un problema. Sin embargo, no los utilizaremos en nuestros ejemplos para simplificar sus explicaciones. Evite lanzar un puntero a un número entero. En el caso de los punteros de 64 bits, la información se perderá si el número entero es de solo cuatro bytes. Los primeros procesadores Intel usaban una arquitectura segmentada de 16 bits donde los punteros cercanos y lejanos eran relevantes. En la arquitectura de memoria virtual actual, ya no son un factor. Los punteros lejano y cercano eran extensiones del estándar C para admitir la arquitectura segmentada en los primeros procesadores Intel. Los punteros cercanos solo podían abordar alrededor de 64 KB de memoria a la vez. Los punteros lejanos podían abordar hasta 1 MB de memoria, pero eran más lentos que los punteros cercanos. Los punteros enormes eran punteros lejanos normalizados, por lo que usaban el segmento más alto posible para la dirección. 2.1.3 Aritmética de apuntadores Operadores de puntero Hay varios operadores disponibles para usar con punteros. Hasta ahora hemos examinado el operador de desreferencia () y la dirección de los operadores (&). En este momento, veremos de cerca la aritmética de punteros y las comparaciones. La Tabla 2.3 resume los operadores de punteros.
ptr += 1; // ptr: 104 printf("%d\n", *ptr); // Muestra 41 ptr += 1; // ptr: 108 printf("%d\n", *ptr); // Muestra 7 Cuando el nombre de una matriz se usa solo, devuelve la dirección de una matriz, que también es la dirección del primer elemento de la matriz: Figura 2.5 Asignación de memoria para un vector. En la siguiente secuencia, agregamos tres al puntero. La variable ptr contendrá la dirección 112, la dirección de ptr: ptr = vector; ptr += 3; El puntero se apunta a sí mismo. Esto no es muy útil, pero ilustra la necesidad de tener cuidado al realizar aritmética de punteros. Acceder a la memoria más allá del final de una matriz es algo peligroso y debe evitarse. No hay garantía de que el acceso a la memoria sea una variable válida. Es muy fácil calcular una dirección inválida o inútil. Las siguientes declaraciones se utilizarán para ilustrar la operación de suma realizada con un tipo short y luego un tipo de datos char : short s; short *ptrS = &s; char c; char *ptrC = &c; Supongamos que la memoria se asigna como se muestra en la figura 2.6. Las direcciones utilizadas aquí están todas en un límite de palabra de cuatro bytes. Las direcciones reales pueden estar alineadas en diferentes límites y en un orden diferente. Figura 2.6 Punteros a short y a char. La siguiente secuencia agrega uno a cada puntero y luego muestra su contenido: printf("Contenido de ptrS antes: %d\n", ptrS); ptrS = ptrS+1; printf("Contenido de ptrS después de: %d\n", ptrS); printf("Contenido de ptrC antes: %d\n", ptrC); ptrC = ptrC+1; printf("Contenido de ptrC después de: %d\n", ptrC); Cuando se ejecuta, debería obtener un resultado similar al siguiente:
Contenido de ptrS antes: 120 Contenido de ptrS despues: 122 Contenido de ptrC antes: 128 Contenido de ptrC despues: 129 El puntero ptrS se incrementa en dos porque el tamaño de un short es de dos bytes. El computador personal puntero se incrementa en uno porque su tamaño es de un byte. Nuevamente, estas direcciones pueden no contener información útil. Punteros para anular y sumar La mayoría de los compiladores permiten que se realice la aritmética en un puntero para anular como una extensión. Aquí supondremos que el tamaño de un puntero a void es cuatro. Sin embargo, intentar agregar uno a un puntero para anular puede generar un error de sintaxis. En el siguiente fragmento de código, declaramos e intentamos agregar uno al puntero: int numero = 5; void *ptrv = № printf("%p\n", ptrv); ptrv = ptrv+1; //Advertencia de sintaxis La advertencia resultante es la siguiente: warning: pointer to type 'void *' used in arithmetic [-Wpointerarith] Dado que esto no es lenguaje C estándar, el compilador emitió una advertencia. Sin embargo, la dirección resultante contenida en ptrv. se incrementará en cuatro bytes. Restar un entero de un puntero Los valores enteros se pueden restar de un puntero de la misma manera que se suman. El tamaño del tipo de datos multiplicado por el valor de incremento entero se resta de la dirección. Para ilustrar los efectos de restar un número entero de un puntero, usaremos una matriz de números enteros como se muestra a continuación. La memoria creada para estas variables se ilustra en la figura 2.7. int vector[] = {28, 41, 7}; int *ptr = vector+2; //pi: 108 printf("%d\n", *ptr); // Muestra 7 ptr--; // pi: 104 printf("%d\n", *ptr // Muestra 41 ptr--; // pi: 100 printf("%d\n", *ptr); // Muestra 28 Cada vez que se resta uno de ptr, cuatro se resta de la dirección. Restar dos punteros Cuando se resta un puntero de otro, obtenemos la diferencia entre sus direcciones. Esta diferencia normalmente no es muy útil excepto para determinar el orden de los elementos en una matriz. La diferencia entre los punteros es el número de "unidades" por las que difieren. El signo de la diferencia depende del orden de los operandos. Esto es consistente con la suma de punteros donde el número agregado es el tamaño del tipo de datos del puntero. Usamos "unidad" como el operando. En el siguiente ejemplo, declaramos una matriz y punteros a los elementos de la matriz. Luego tomamos su diferencia:
// ptr2>ptr0: 1 // ptr2<ptr0: 0 // ptr0>ptr1: 0 Usos comunes de los punteros Los punteros se pueden utilizar de varias maneras. En esta sección, examinaremos diferentes formas de usar punteros, que incluyen: Múltiples niveles de direccionamiento indirecto Punteros constantes 2.1.4 Manejo de arreglos con apuntadores Múltiples niveles de indirección Los punteros pueden usar diferentes niveles de direccionamiento indirecto. No es raro ver una variable declarada como un puntero a un puntero, a veces llamado puntero doble. Un buen ejemplo de esto es cuando los argumentos del programa se pasan a la función principal main() usando los tradicionalmente parámetros llamados argc y argv. El siguiente ejemplo utiliza tres matrices. La primera matriz es una matriz de cadenas que se utiliza para contener una lista de títulos de libros: char *titulos[] = { "Un cuento de dos ciudades", "Cumbres borrascosas", "Don Quijote", "Odisea", "Moby Dick", "Hamlet", "Las travesias de Gulliver" }; Se proporcionan dos matrices adicionales cuyo propósito es mantener una lista de los "mejoresLibros" y “librosDeIngles”. En lugar de tener copias de los títulos, tendrán la dirección de un título en la información titulos. Ambas matrices deberán declararse como un puntero a un puntero a un tipo char. Los elementos de la matriz contendrán las direcciones de la matriz titulos. Esto evitará tener que duplicar la memoria para cada título y dará como resultado una única ubicación para los títulos. Si es necesario cambiar un título, el cambio solo tendrá que realizarse en una ubicación. Las dos matrices se declaran a continuación. Cada elemento de la matriz contiene un puntero que apunta a un segundo puntero a char se: char **mejoresLibros[3]; char **librosDeIngles[4]; Las dos matrices se inicializan y se muestra uno de sus elementos, como se muestra a continuación. En las sentencias de asignación, el valor del lado derecho se calcula aplicando primero los subíndices, seguidos del operador de dirección. Por ejemplo, la segunda declaración asigna la dirección del cuarto elemento de títulos al segundo elemento de mejores libros: mejoresLibros[0]= &titulos[0]; mejoresLibros[1]= &titulos[3]; mejoresLibros[2]= &titulos[5];
librosDeIngles[0]= &titulos[0]; librosDeIngles[1]= &titulos[1]; librosDeIngles[2]= &titulos[5]; librosDeIngles[3]= &titulos[6]; printf("%s\n", *librosDeIngles[1]); // Cumbres borrascosas La memoria se asigna para este ejemplo como se muestra en la figura 2.8. Figura 2.8 Punteros a punteros El uso de múltiples niveles de direccionamiento indirecto proporciona flexibilidad adicional en la forma en que se puede escribir y usar el código. Ciertos tipos de operaciones serían de otro modo más difíciles. En este ejemplo, si la dirección de un título cambia, solo requerirá modificación a la información titulos formación. No tendríamos que modificar las otras matrices. No hay un límite inherente en el número de niveles de direccionamiento indirecto posibles. Por supuesto, usar demasiados niveles de direccionamiento indirecto puede ser confuso y difícil de mantener. Constantes y punteros Utilizando la palabra clave const con punteros es un aspecto rico y poderoso de C. Proporciona diferentes tipos de protecciones para diferentes conjuntos de problemas. De particular poder y utilidad es un puntero a una constante. luego, veremos cómo esto puede proteger a los usuarios de una función de la modificación de un parámetro por parte de la función. Punteros a una constante Un puntero se puede definir para que apunte a una constante. Esto significa que el puntero no se puede utilizar para modificar el valor al que hace referencia. En el siguiente ejemplo, se declaran un entero y una constante entera. A continuación, se declaran un puntero a un entero y un puntero a una constante entera y luego se inicializan a los enteros respectivos: int numero = 5; const int limite = 500; // Puntero a un entero int * ptr; cons int * ptrci; // Puntero a un entero constante ptr = № ptrci = &limite; Esto se ilustra en la figura 2.9.