Ficheros en C++

Para trabajar con ficheros en C++ debe incluirse la cabecera fstream.

Dependiendo de lo que deseemos hacer con el fichero, usaremos objetos de las clases:

  • ifstream ( input file stream), clase orientada para la lectura

  • ofstream (output file stream). clase orientada para la escritura

  • fstream (file stream), cuando deseemos alternativamente leer o escribir del mismo fichero en el mismo programa.

A diferencia de cin y cout, que son objetos predefinidos de acceso global, listos para su uso, los objetos que representan flujos de datos a/desde ficheros deben ser definidos por el programador.

La apertura del fichero consiste en definir un objeto de la clase deseada (ifstream, ofstream ó fstream).

Para escribir o leer, se usan los operadores de inserción << y extracción >> como si del teclado o consola se tratase.

El fichero se cierra implícitamente cuando el objeto sale del ámbito en el que se ha definido o explícitamente llamando a la función miembro close().

En lo que sigue vamos a ver un pequeño subconjunto de las posibilidades que ofrece C++, y solo para trabajar con ficheros tipo texto.

Para muchos problemas, con las herramientas que aquí se estudian, es más que suficiente.

Escritura de un fichero tipo texto

Ejemplo: escritura en un fichero de los 10 primeros enteros

#include <fstream>  // Para ofstream
#include <iostream> // Para cout
using namespace std;

int main()
{
  ofstream fich("ejemplo.dat");
  if (!fich)
  {
    cout << "Error al abrir ejemplo.dat\n";
    exit(EXIT_FAILURE);
  }

  for (int i = 0; i < 10; ++i)
    fich << i << endl;
}

Edita, compila y ejecuta el código

Analicemos las líneas más significativas:

  • ofstream fich("ejemplo.dat");

    ofstream es una clase que permite crear la instancia fich para escribir en un fichero. En este caso, se crea un fichero para escritura llamado "ejemplo.dat".

    Es conveniente saber dónde el IDE va a crear o leer por defecto el archivo. En el caso de Code::Blocks, el directorio por defecto es el del proyecto.

  • if (!fich){...}

    Verificar si se ha creado el objeto fich. Si hay error, el operador ! devuelve true.

  • exit(EXIT_FAILURE);

    En general, y en el marco de la asignatura, en presencia de un error al intentar abrir el fichero, se optará por dar por finalizado el programa. Otra opción podría ser volver a solicitar el nombre del fichero al usuario del programa.

  • fich << i << endl;

    A la hora de trabajar con el fichero, la operativa es idéntica a la que se usa con cout. En este caso, el papel del objeto estándar para la salida cout se sustituye por fich, y los datos, en lugar de dirigirse hacia la consola, se almacenan en el fichero "ejemplo.dat". Nótese que basta sustituir fich por cout para visualizar el aspecto que tendrán en el archivo los datos.

Nótese que no se ha procedido a cerrar el fichero mediante una sentencia del tipo fich.close(). En este ejemplo no es necesario, aunque cerrarlo explícitamente es totalmente legítimo. Lo que ocurre es que cuando cualquier objeto en C++ sale del ámbito en el que se encuentra declarado, una función interna llamada destructor se encarga automáticamente de liberar los recursos que ese objeto haya podido acaparar. En este caso, el destructor de la clase ofstream se encarga de cerrar el fichero, aunque nosotros no lo hayamos hecho.

Lectura de un fichero tipo texto

Ejemplo 2: lectura de un fichero con enteros: Variante 1

En esta variante, se sabe que el fichero tiene números enteros, separados por espacios en blanco, ¡pero no sabemos cuántos!

../../_images/ficheros4.jpg

Fichero utilizado en el ejemplo

#include <fstream>  // Para ifstream
#include <iostream> // Para cout
#include <vector>
using namespace std;

void muestra_vector(const vector<int>&);
int main()
{
  ifstream fich("ejemplo.dat");
  if (!fich.is_open())
  {
    cout << "Error al abrir ejemplo.dat\n";
    exit(EXIT_FAILURE);
  }

  int valor;
  vector<int> datos;
  while (fich >> valor)
    datos.push_back(valor);
  muestra_vector(datos);
}

void muestra_vector(const vector<int>& v)
{
  for (auto x : v)
    cout << x << " ";
  cout << endl;
}

Edita, compila y ejecuta el código

Analicemos las líneas más significativas:

  • ifstream fich("ejemplo.dat");

    ifstream es una clase que permite crear la instancia fich para leer de un fichero. En este caso, se abre un fichero para lectura llamado "ejemplo.dat".

  • if (!fich.is_open())

    Es otra forma similar a !fich para verificar que se ha podido abrir el archivo.

  • while (fich >> valor){...}

    Dado que no sabemos el número de elementos, incluso el archivo puede estar vacío, utilizamos un bucle while.

    Cuando el operador de extracción >> llega al final del fichero fich, la operación se evalúa como false, lo que permite salir del bucle.

    De nuevo, destacar que el papel que tenía cin en la lectura desde el teclado ahora es asumido por la lectura desde el fichero fich.

Ejemplo 3: lectura de un fichero con enteros: Variante 2

A diferencia del ejemplo anterior, ahora el formato del archivo informa en primer lugar del número de elementos que componen la secuencia de enteros. Esto suele ser una ventaja, dado que nos permite reservar de antemano espacio para nuestro vector.

../../_images/ficheros5.jpg

Fichero utilizado en el ejemplo

#include <fstream>  // Para ifstream
#include <iostream> // Para cout
#include <vector>
using namespace std;
void muestra_vector(const vector<int>&);
int main()
{
  ifstream fich("ejemplo.dat");
  if (!fich.is_open())
  {
    cout <<"Error al abrir ejemplo.dat\n";
    exit(EXIT_FAILURE);
  }
  int num_elementos;
  fich >> num_elementos;
  vector<int> datos;
  datos.reserve(num_elementos);
  for (int i = 0; i < num_elementos; ++i)
  {
    int valor;
    fich >> valor;
    datos.push_back(valor);
  }
  muestra_vector(datos);
}
void muestra_vector(const vector<int>& v)
{
  for (auto x : v)
    cout << x << " ";
  cout << endl;
}

Edita, compila y ejecuta el código

Analicemos las líneas más significativas:

  • int num_elementos;

  • fich >> num_elementos;

    Leemos el primer dato del archivo que, para este formato, es el número de elementos.

  • datos.reserve(num_elementos);

Es opcional su uso, pero evitamos tras las bambalinas que vector tenga que ir solicitando al S.O. nuevo espacio en memoria conforme la capacidad del vector se sobrepasa.

  • for (int i = 0; i < num_elementos; ++i)

    Ahora sabemos el número de elementos a leer y, por lo tanto, un bucle for es el más adecuado para esta tarea.

En los dos ejemplos anteriores se ha utilizado un fichero con los datos deliberadamente dispersos, con múltiples e innecesarios espacios en blanco y caracteres nueva línea. El único objetivo es resaltar que, al igual que ocurre con cin, el proceso de extracción ignora este tipo de caracteres. En condiciones normales, lo habitual es utilizar un fichero con alguna de las distribuciones mostradas en la figura.

Ficheros con número de elementos como primer dato

Opción 1

Opción 2

../../_images/ficheros6.jpg
../../_images/ficheros7.jpg

Leyendo ficheros con un formato complejo

Los ejemplos anteriores basados en enteros trabajan con ficheros que poseen una única secuencia de valores.

Las aplicaciones prácticas requieren ser capaces de crear y leer ficheros que tengan una estructura compleja conocida de tamaño arbitrario, con independencia del número de datos que estos ficheros almacenen. En estos ficheros pueden mezclarse comentarios y datos, tanto numéricos como cadenas de caracteres.

Algunos ejemplos entre los innumerables que podrían citarse, pueden ser:

  • los datos del censo de una ciudad o un país: nombre y apellidos, DNI, dirección y edad de los votantes.

  • la sucesión de temperaturas recogidas por diferentes sensores en diferentes lugares.

  • las filas y columnas de una matriz de dimensiones arbitrarias.

Todos los problemas tienen en común que el significado de los datos y la estructura básica de la organización de los mismos tienen que ser conocidos por el programador.

El volumen total de los datos (cantidad de personas, número de lecturas del sensor, dimensiones de la matriz) es, en general, desconocido. Sin embargo, como ya hemos apuntado en un apartado anterior, añadir al formato metadatos suele ser de gran ayuda. El ejemplo sobre el almacenamiento de una matriz visto en un apartado anterior es relevante: el formato incluye inicialmente el número de filas y columnas.

Nos limitaremos en esta asignatura a trabajar con ficheros tipo texto donde los datos de interés están separados por espacios en blanco.

Para ilustrar las dificultades que surgen para ficheros con formatos demasiado complejos, vamos a analizar el aparentemente simple ejemplo siguiente.

Ejemplo 4: lectura de un fichero con cadenas de caracteres

El fichero mostrado en la figura almacena el nombre y apellidos de una serie de personas. En la primera línea, tiene el número de personas almacenadas.

../../_images/ficheros8.jpg

Para la lectura vamos a ensayar el uso de string con la intención de almacenar los nombres y apellidos. Este programa, del que mostramos un extracto, es una variante del ejemplo anterior, cambiando int por string.

Veamos lo que ocurre:

...
int main()
{
  ifstream fich("nombre_apellidos.txt");
  ...

  int num_elementos;
  fich >> num_elementos;
  vector<string> datos;
  datos.reserve(num_elementos);
  for (int i = 0; i < num_elementos; ++i)
  {
    string valor;
    fich >> valor;
    datos.push_back(valor);
  }
  muestra_vector(datos);
}

Edita, compila y ejecuta el código

Como era de esperar, no se produce el resultado deseado. El programa sólo ha sido capaz de almacenar en el vector los valores {Enrique, López, Beneito}. No debemos olvidar que el operador de extracción >> procesa el flujo de datos entre dos caracteres distintos que espacios en blanco. Los espacios en blanco, incluido el carácter nueva línea, son ignorados. Desde la óptica del operador de extracción >> el fichero en realidad tiene 13 datos, 13 cadenas de caracteres.

Una solución para este problema es usar la función getline() de la biblioteca string, que permite extraer de un fichero bloques de texto, por ejemplo, correspondientes a líneas.

En general, cuando de un fichero se necesitan extraer cadenas de caracteres que incluyen espacios en blanco, como apellidos compuestos, tipo de la Torre, o valores separados por comas, etc. el análisis (parsing) del texto da lugar a código relativamente complejo y que no merece la pena estudiar en un curso básico.

Veamos algún ejemplo de formato de archivo complejo más simple y no por ello menos útil.

Ejemplo 5: Clasificación de pilotos

Se dispone de un fichero con nombre «pilotos_y_clasificaciones.txt» que almacena las clasificaciones de los pilotos de una competición automovilística.

El formato es siguiente:

  • en la 1ª línea se almacena el número de carreras disputadas

  • en la 2ª línea el número total de pilotos

  • a partir de las dos primeras líneas, aparecen grupos de 2 líneas:

    • una con el nombre del piloto

    • otra con los puestos conseguidos, separados por un espacio, en las carreras disputadas hasta el momento. Un 0 en la línea de puestos significa que el piloto no finalizó la carrera.

En un fichero inicial, con 0 carreras disputadas, las líneas con los puestos no aparecen.

El orden de los pilotos en el fichero no es relevante.

El objetivo es obtener la puntuación actual de cada uno de los pilotos. El sistema de puntuación en cada carrera es {10,8,6,5,4,3,2,1}, siendo 10 la puntuación conseguida por el piloto que acaba en la 1ª posición, 8 puntos la del que acaba en 2ª posición y así hasta 1 punto para el piloto que finaliza en 8ª posición. Por tanto, el resto de pilotos no consiguen puntos.

En la tabla se representan dos ejemplos del fichero en dos situaciones: la inicial y otra con 4 carreras disputadas.

Pilotos y clasificaciones

Sin carreras disputadas

4 carreras disputadas

../../_images/ficheros9.jpg
../../_images/ficheros10.jpg

Aunque C++ permite formas más compactas y eficientes de resolver este problema, nos contentaremos con obtener dos vectores:

  • uno con las cadenas de caracteres correspondientes a los pilotos

  • otro con sus puntuaciones acumuladas, en posiciones correlativas a la de los pilotos

Para ello utilizaremos una función con prototipo:

void puntuacion(vector<string>& pilotos, vector<int>& puntos, string nombre_fichero);

Dado que el fichero contiene los puestos conseguidos y no las puntuaciones, utilizaremos una función con prototipo:

int calcula_puntuacion(const vector<int>& puestos);

que nos permita obtener la puntuación a partir de los puestos.

Una posible implementación es la que sigue:

#include <fstream>  // Para ifstream
#include <iostream> // Para cout
#include <vector>
using namespace std;

void puntuacion(vector<string>& pilotos, vector<int>& puntos, string nombre_fichero);
int calcula_puntuacion(const vector<int>& puestos);
void muestra_puntuacion(const vector<string>&, const vector<int>&);

int main()
{
  vector<string> pilotos;
  vector<int> puntos;
  puntuacion(pilotos, puntos, "pilotos_clasificaciones.txt");
  muestra_puntuacion(pilotos, puntos);
}

void puntuacion(vector<string>& pilotos, vector<int>& puntos, string nombre_fichero)
{
  ifstream fich(nombre_fichero);
  if (!fich.is_open())
  {
    cout <<"Error al abrir " << nombre_fichero << "\n";
    exit(EXIT_FAILURE);
  }

  int num_carreras;
  fich >> num_carreras;
  int num_pilotos;
  fich >> num_pilotos;

  pilotos.clear();  // Por si vienen con datos a la función
  puntos.clear();

  for (int i = 0; i < num_pilotos; ++i)
  {
    string piloto;
    fich >> piloto;
    pilotos.push_back(piloto);
    vector<int> puestos;
    for (int j = 0; j < num_carreras; ++j)
    {
      int puesto;
      fich >> puesto;
      puestos.push_back(puesto);
    }
    puntos.push_back(calcula_puntuacion(puestos));
  }
}


int calcula_puntuacion(const vector<int>& puestos)
{
  const vector<int> puntos{10,8,6,5,4,3,2,1};

  int suma = 0;
  for (auto puesto : puestos)
    if (puesto > 0 && puesto < puntos.size() + 1)
      suma += puntos[puesto - 1];
  return suma;
}

void muestra_puntuacion(const vector<string>& pilotos, const vector<int>& puntos)
{
  cout << "\nLa puntuación actual es:\n\n";
  for (int i = 0; i < pilotos.size(); ++i)
    cout << pilotos[i] << " " << puntos[i] << endl;
  cout << endl << endl;
}

Edita, compila y ejecuta el código

Es importante tener claro que la lectura de un fichero es secuencial. Si los datos del fichero hubiesen sido introducidos por teclado, bastaría sustituir fich >> ... por cin >> .... La única diferencia es que cuando se introducen los datos por teclado, previamente se muestra un mensaje con cout al usuario indicando qué valor debe introducir. En el caso de un fichero, donde se presupone que el programador conoce el formato del fichero, esta información que aporta el cout es implícita.

Modos de apertura

En todos los ejemplos previos hemos utilizado los modos de apertura de un fichero por defecto.

  • La clase ofstream por defecto crea ficheros tipo texto para escritura. Si el fichero existe, equivale a su borrado previo.

  • La clase ifstream por defecto abre ficheros tipo texto para lectura. Si el fichero no existe, se generará un error.

C++ permite especificar explícitamente otros modos (flags) de apertura. Están definidos en el espacio de nombres ios. Algunos de ellos son:

  • ios:: binary: El fichero será binario

  • ios:: in: Fichero modo lectura

  • ios:: out: Fichero modo escritura

  • ios:: app: Fichero en modo añadir (append)

De entre estos modos, nos interesa el modo ios:: app. Este modo permite añadir datos al final de un fichero ya existente y, por tanto, no se pierden los datos anteriores.

Este modo es muy interesante porque permite trabajar en modo escritura con un fichero en diferentes sesiones. Un posible uso del modo añadir sería ofstream fich(nombre_fichero, ios::app);.

Veamos un ejemplo.

Ejemplo 6: Añadir a un fichero números enteros

#include <fstream>  // Para ofstream
#include <iostream> // Para cout
using namespace std;

void guarda_entero(int valor, string nombre_fichero);
int main()
{
  for (int i = 0; i < 10; ++i)
    guarda_entero(i, "ejemplo.txt");
}

void guarda_entero(int valor, string nombre_fichero)
{
  ofstream fich{nombre_fichero, ios::app};
  if (!fich)
  {
    cout << "Error al abrir " << nombre_fichero << endl;
    exit(EXIT_FAILURE);
  }
  fich << valor << endl;
}

Edita, compila y ejecuta el código

Nótese que en cada llamada a guarda_valor() se abre y cierra el fichero: la bandera ios::app permite que el contenido anterior del fichero no se pierda.