Manejo de archivos
13. Acceso Aleatorio.
13.3. Relación entre punteros y los métodos read() y write()
Hasta ahora hemos estudiado
- punteros, punteros constantes y punteros a datos constantes.
- mencionamos el acceso aleatorio a archivos.
El por que de esto se justifica con el tema que sigue.
Los métodos para acceder de manera aleatoria a archivo, en esos caso se usan métodos write y read , y los argumentos de esos métodos son del tipo que vimos en la sección anterior: punteros constantes a caracter o punteros a caracteres constantes.
¿Por qué los métodos read() y write() utilizan punteros a char?
Cuando trabajamos con archivos binarios, los métodos read() y write() no piensan en términos de variables de tipo int, float o double. En realidad, solamente copian secuencias de bytes entre la memoria RAM y el archivo.
Recordemos que la unidad básica de almacenamiento de información es el byte.
Por ejemplo:
char c; // suele ocupar 1 byte
int x; // suele ocupar 4 bytes
float y; // suele ocupar 4 bytes
double z; // suele ocupar 8 bytes
Aunque estos tipos representan datos diferentes, todos están almacenados en memoria como una secuencia de bytes.
Supongamos:
int numero = 1000;
En una computadora donde un int ocupa 4 bytes, la memoria podría verse conceptualmente así:
Dirección Contenido
#A byte 0
#B byte 1
#C byte 2
#D byte 3
Cuando queremos guardar este entero en un archivo binario, el método write() no necesita saber que se trata de un int. Lo único que necesita saber es:
-
Dónde comienzan los bytes: #A
-
Cuántos bytes debe copiar : 4
Por eso utilizamos:
int numero = 1000;write((const char *)&numero, sizeof(numero));
Analicemos cada parte:
&numero
obtiene la dirección de memoria donde comienza el entero
Su tipo es:
int *
Sin embargo, write() trabaja byte a byte, para poder recorrer los bytes del entero, la dirección se convierte a:
const char *
mediante: (const char *)&numero
Esta conversión le indica al método:
"Considere esta dirección como el comienzo de una secuencia de bytes."
A continuación el otro argumento:
sizeof(numero)
indica cuántos bytes debe copiar.
Si el entero ocupa 4 bytes, write() copiará exactamente esos 4 bytes al archivo.
¿Por qué se utiliza char?
En C y C++, el tipo char tiene una característica fundamental:
sizeof(char) == 1
Es decir, un objeto de tipo char ocupa exactamente un byte.
Por esta razón, un puntero a char puede utilizarse para recorrer la memoria byte a byte.
Por ejemplo: char *p;
Si: p++;
el puntero avanza exactamente un byte.
En cambio:
int *p;
p++;
avanza tantos bytes como ocupe un entero (normalmente 4).
Como los archivos binarios están formados por secuencias de bytes, el tipo más adecuado para acceder a ellos es char *.
¿Qué ocurre durante la lectura?
Cuando ejecutamos:
int numero;read((char *)&numero, sizeof(numero));
el proceso es el inverso, numero es un int, obtengo la dirección del byte de inicio, y lo convierto a char para tomar solo el primer byte, luego sizeof(numero) me indica la cantidad de bytes que ocupa numero. En este caso NO está la palabra const por que tiene que escribir los bytes indicados por sizefo a partir del 1er byte indicado, por lo tanto NO puede ser const !!!
El método:
-
Lee bytes desde el archivo.
-
Los copia en la dirección indicada por
&numero. -
Reconstruye el valor almacenado en la variable.
Por esta razón read() recibe un char *, ya que necesita modificar los bytes almacenados en memoria.
Idea fundamental
- Los métodos
read()ywrite()no leen ni escriben variables. - Los métodos
read()ywrite()leen y escriben conjuntos de bytes.
Por eso reciben direcciones convertidas a punteros char * o const char *, que permiten acceder a la memoria byte a byte.
Por ejemplo:
flujo_salida.write((const char *)&temperatura, sizeof(float));
flujo_entrada.read((char *)&temperatura, sizeof(float));
La razón es sencilla: para escribir o leer datos binarios, los métodos write() y read() necesitan conocer la dirección de memoria donde se encuentran los datos.
Por eso se utiliza el operador dirección:
&temperatura
que devuelve la dirección de memoria de la variable.
Si temperatura es un float, entonces:
&temperatura
es de tipo:
float *
Sin embargo, los métodos read() y write() trabajan byte a byte, por lo que esperan recibir un puntero a caracteres:
char *
o
const char *
Por esta razón aparece una conversión explícita:
(char *)&temperatura // para lectura o read si fuera constante no podría volcar el contenido leido
o
(const char *)&temperatura // para escritura o write
¿Por qué write() utiliza const char *?
Al escribir un archivo:
flujo_salida.write((const char *)&temperatura, sizeof(float));
el método solamente necesita leer los bytes almacenados en memoria para copiarlos al archivo, no necesita modificar los datos que tiene en la memoria al pasar al archivo para cumplir su tarea , por esa razón recibe un:
const char *
es decir, un puntero a datos constantes.
¿Por qué read() utiliza char *?
Al leer un archivo:
flujo_entrada.read((char *)&temperatura, sizeof(float));
el método debe copiar información desde el archivo hacia la memoria , por lo tanto necesita modificar el contenido de la variable.
Por esa razón recibe un:
char *
es decir, un puntero a datos modificables.
Resumen
write() ---> const char *
"Voy a leer los datos apuntados, pero no los modificaré."
read() ---> char *
"Voy a escribir datos en la dirección apuntada."
Por lo tanto, los conceptos de punteros a datos constantes y punteros a datos modificables estudiados previamente aparecen nuevamente en el manejo de archivos binarios.