13. Acceso Aleatorio.

13.3. Relación entre punteros y los métodos read() y write()

Hasta ahora hemos estudiado

  1. punteros, punteros constantes y punteros a datos constantes. 
  2. 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:

  1. Lee bytes desde el archivo.

  2. Los copia en la dirección indicada por &numero.

  3. 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() y write() no leen ni escriben variables.
  • Los métodos read() y write() 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

(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.

Un archivo binario no sabe qué es un int, un float o una estructura. Para él todo son bytes. Por eso read() y write() trabajan con punteros a char, que permiten recorrer la memoria byte a byte.