Zapis wyników do pliku w C, C++ i Javie

Poniżej zostaną przedstawione przykładowe programy odpowiednio w C, C++ i Javie zapisujące wartości zmiennoprzecinkowe z określoną dokładnością lub liczbą cyfr po przecinku do określonego pliku tekstowego. Nacisk został położony na prostotę przykładu, tak więc n.p. obsługa błędów jest mocno ograniczona. Przykładowe zastosowanie podanych wzorców to zapis wyników obliczeń numerycznych do pliku w celu zrobienia z nich wykresu n.p. programem gnuplot.

Wszystkie podane przykłady zostały przetestowane przy pomocy gcc (GNU Compiler Collection).

Zapis do pliku przy użyciu biblioteki standardowej C

Poniżej znajduje się przykładowy program w C, zapisujący kolejne elementy tablic do pliku tekstowego c.dat. Nagłówek funkcji main podany jest w postaci umożliwiającej odczyt parametrów wywołania, z czego w programie nie korzystamy. Format wypisywania danych "% .12g" oznacza wypisywanie liczby zmiennoprzecinkowej w postaci ogólnej (general), tzn. albo w postaci stałoprzecinkowej, albo w postaci naukowej/wykładniczej (przy użyciu e jako oznaczenia potęg 10; aby uzyskać E należy użyć %G zamiast %g), z precyzją 12 cyfr (ilością cyfr użytą do zapisu liczby). Spacja po znaku procenta oznacza poprzedzenie liczby spacją jeśli liczba nie zaczyna się od znaku plus lub minus. Rozmiar tablicy ustalony jest na N=5 elementów; zamiast użycia stałej preprocesora można by użyć odpowiedniej makrodefinicji do odczytu rozmiaru tablicy; rozmiar byłby ustalany przez postać definicji tablicy.

Program był kompilowany za pomocą gcc 3.3.2, przy użyciu biblioteki standardowej glibc 2.3.2 (libc.so.6).

/* file-output.c */
#include <stdio.h>
#define N 5

int main(int argc, char *argv[])
{
  FILE *f;
  
  int i;
  float x[] = {-2, -1, 0, 1, 2};
  float y[] = { 2,  1, 0, 1, 2};
      

  f = fopen("c.dat", "w");
  if (f == NULL) {
    perror("Nie udalo sie otworzyc pliku 'c.dat' do zapisu");
    return 1;
  }
  
  fprintf(f, "# file-output.c\n");
  for (i = 0; i < N; i++)
    fprintf(f, "x = % .12g, y = % .12g\n", x[i], y[i]);
  
  fflush(f);

  fclose(f);

  return 0;
}

Zapis przy użyciu strumieni (interfejs obiektowy) w C++

Poniżej znajduje się przykładowy program w C++, zapisujący kolejne elementy tablic do pliku tekstowego cpp.dat. Program ten jest identyczny co do postaci z poprzednim programem w czystym C (powyżej), ale wykorzystuje obiektowe (z wykorzystaniem strumieni) operacje wejścia/wyjścia. W porównaniu z programem w C brakuje mu wypisywania szczegółowych informacji o przyczynie braku możliwości otwarcia pliku (to co w C można zrealizować np. za pomocą funkcji perror). Format ogólny dla liczb zmiennoprzecinkowych jest formatem domyślnym. Precyzję wyprowadzania danych ustaliliśmy za pomocą manipulatora setprecision. W porównaniu z programem w C brakuje wypisywania spacji przed liczbami dodatnimi. Byłoby proste uogólnić ten program by stosował tablice o zmiennym rozmiarze, alokowane i zwalniane za pomoca operatorów new i delete.

Otwarcia pliku można by dokonać podając jego nazwę bezpośrednio w konstruktorze:

    ofstream f ("cpp.dat");
Wtedy niepotrzebne byłoby (chyba) ręczne zamykanie pliku za pomocą metody close(); byłoby ono dokonywane automatycznie przez destruktor obiektu. Ustalenia precyzji można dokonać także bez manipulatorów za pomocą metody precision:
    f.precision(12);
Zamiast manipulatora f << flush można stosować metodę f.flush(). Aby móc używać endl bez dołączania manipulatorów #include <iomanip.h> trzeba by zapewne zmienić #include <iostream>  na  #include <iostream.h>. Obiektowe wejście/wyjście można przyspieszyć rezygnując ze współpracy z biblioteką IO w C, za pomocą wywołania funkcji sync_with_stdio(false);

Obiektowe wejście/wyjście można łatwo uogólnić na inne klasy, np. liczby zespolone, wektory, macierze czy wielomiany (lub n.p. punkty interpolacji).

Program był kompilowany za pomocą g++ (GCC) 3.3.2, przy użyciu bibliotek libstdc++ 3.3.2 (libstdc++.so.5.0.5), libgcc 3.3.2 (libgcc_s.so.1) i glibc 2.3.2 (libc.so.6). Zestaw plików nagłówkowych (w tym użycie przestarzałych plików nagłówkowych z rozszerzeniem .h) jest dostosowany do tego kompilatora.

// file-output.cc
#include <iostream>
#include <fstream.h>
#include <iomanip.h>

#define N 5

int main(int argc, char *argv[])
{
    ofstream f;
    
    float x[] = {-2, -1, 0, 1, 2};
    float y[] = { 2,  1, 0, 1, 2};

    f.open("cpp.dat");
    // rownowazne f.open("cpp.dat", ios::out | ios::trunc);
    if (!f) {
        cerr << "Nie udalo sie otworzyc pliku 'cpp.dat' do zapisu" << endl;
        return 1;
    }
    
    f << "# file-output.cc" << endl;
    f << setprecision(12);
    for (int i = 0; i < N; i++)
        f << "x = " << x[i] << ", y = " << y[i] << endl;

    f << flush;

    f.close();
}

Zapis do pliku w Javie

Poniżej znajduje się przykładowy program w Javie, zapisujący liczby zmiennoprzecinkowe w postaci niesformatowanej (sformatowane domyślnie) i sformatowane (z dokładnością 10 cyfr po przecinku) do pliku java.dat. Program wypisuje szczegółowe informacje (łącznie z nazwą pliku) o przyczynie braku możliwości otwarcia pliku. Przykładowy program podany poniżej jest może nie najlepszy, ale prosty.

Program był kompilowany za pomocą gcj (GCC) 3.3.2 (do postaci pliku wykonywalnego, a nie tylko byte-code wykonywalnego w maszynie wirtualne Javy), przy użyciu bibliotek libgcj 3.3.2 (libgcj.so.4), zlib 1.2.0.7 (libz.so.1), libgcc 3.3.2 (libgcc_s.so.1) i glibc 2.3.2 (libpthread.so.0, libdl.so.2, libc.so.6). Postać wywołania kompilatora podana jest w przykładowym pliku Makefile; komentarze co do nazwy klasy publicznej która musi być taka sama jak nazwa pliku bez rozszerzenia dotyczy tego kompilatora (ale może to być też wymaganie języka).

// file_output.java
import java.io.*;   // potrzebne do zapisu do pliku
import java.text.*; // potrzebne do sformatowania wyników
import java.util.*; // potrzebne do wyboru locale

public class file_output // taka sama nazwa jak nazwa pliku bez rozszerzenia .java 
{
    
    public static void main(String[] args) 
    {
        double x=2.1, y=3.1787652;
        
        PrintWriter outfile = null; // inicjalizacja konieczna
        NumberFormat nf = NumberFormat.getNumberInstance(Locale.US); 
           // aby separatorem dziesietnym byla kropka '.'

        try { // try/catch konieczny
            outfile = new PrintWriter(new FileWriter("java.dat"));
        } catch (IOException e) {
            System.out.println("Pliku nie udalo sie otworzyc: " + e.getLocalizedMessage());
            System.exit(1);
        }
        
        nf.setMinimumFractionDigits(10);
        nf.setMaximumFractionDigits(12);

        // print   = bez znaku konca linii
        // println = ze znakiem konca linii
        
        outfile.println("# file_output.java");
        outfile.println("#");
        outfile.println("# niesformatowane wyjscie");
        outfile.println("x = " + x + ", y = " + y);
        outfile.println("# sformatowane wyjscie");
        outfile.println("x = " + nf.format(x) + ", y = " + nf.format(y));       

        outfile.close(); // konieczne!
    }
}

Poniżej natomiast znajduje się równoważnik (mniej więcej) podanych programów w C i C++

// FileOutputExample.java
import java.io.*;   // potrzebne do zapisu do pliku
import java.text.*; // potrzebne do ew. sformatowania wyników
import java.util.*; // potrzebne do wyboru locale

public class FileOutputExample // taka sama nazwa jak nazwa pliku bez rozszerzenia .java 
{
    
    public static void main(String[] args) 
    {
        float[] x = {-2, -1, 0, 1, 2};
        float[] y = { 2,  1, 0, 1, 2};
        int N = x.length;

        PrintWriter f = null; // inicjalizacja konieczna
        NumberFormat nf = NumberFormat.getNumberInstance(Locale.US); 
           // aby separatorem dziesietnym byla kropka '.'


        try { // try/catch konieczny
            f = new PrintWriter(new FileWriter("FileOutputExample.dat"));
        } catch (IOException e) {
            System.out.println("Pliku nie udalo sie otworzyc: " + e.getLocalizedMessage());
            System.exit(1);
        }

  	nf.setGroupingUsed(false);       // nie używamy separatora tysięcy
        nf.setMaximumFractionDigits(12); // ustalamy liczbę cyfr po przecinku (nie dokładność!)

        f.println("# FileOutputExample.java");
        for (int i = 0; i < N; i++) 
            f.println("x = " + nf.format(x[i]) + ", y = " + nf.format(y[i]));
        
        f.flush();

        f.close(); // konieczne!
    }
}

Makefile

Poniżej znajduje się plik Makefile, użyty do kompilowania powyższych przykładów. Uwaga: poniżej oznaczenie          przed poleceniami do wykonania dla określonego celu (ang. target) to jeden znak tabulacji!

# Makefile
CC  = gcc
CXX = g++
JAVA= gcj

CFLAGS    = -Wall
CXXFLAGS  = -Wall
JAVAFLAGS = -Wall

LDLIBS = -lm


all: file-output_c file-output_cc file_output file-output_java

file-output_c:  file-output.c
	$(CC) $(CPPFLAGS) $(CXXFLAGS) -o $@ $^

file-output_cc:  file-output.cc
	$(CXX) $(CPPFLAGS) $(CXXFLAGS) -o $@ $^

file_output:  file_output.java
	$(JAVA) $(CPPFLAGS) $(JAVAFLAGS) --main=$@ -o $@ $^

file-output_java:  FileOutputExample.java
	$(JAVA) $(CPPFLAGS) $(JAVAFLAGS) --main=$(basename $< .java) -o $@ $^


Wszystkie przykłady powinny się nadawać do bezpośredniego przekopiowania.
Jakub Narębski
e-mail: Jakub.Narebski@fuw.edu.pl

Powrót do strony domowej