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.
- No puedo escuchar las frecuencias que debería escuchar mi grupo de edad, ¿debo hacer que me revisen los oídos?
- Mi Nexus 5 tiene un problema de parpadeo de la pantalla. ¿Cómo puedo obtener un reemplazo de garantía? ¿Qué tengo que hacer?
- Estoy buscando una guía de seguridad de TI pragmática, corta y comprensible en alemán o inglés. ¿Alguna recomendación?
- Me estoy preparando para mi ingreso médico. Voy a dar NEET 2 el 24 de julio. ¿Cómo me preparo en un mes?
- ¿Por qué me queman los ojos después de sacar mis contactos? ¿Cómo puedo hacer que pare?
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.