Wpadki programistyczne

Konwersja liczby całkowitej na nazwę pliku
W programie drukowania kalendarza do pliku tekstowego, zaprezentowanym w drugim wydaniu książki Turbo Pascal i Borland C++. Przykłady (Helion, 2006), wczytana z klawiatury liczba całkowita r reprezentująca numer roku jest zamieniana na nazwę pliku postaci RRRR.TXT. Wersja programu w języku C zawiera funkcję biblioteczną sprintf(), która doskonale nadaje się do tego zadania. Wywołana z formatem %d.txt wyprowadza wartość zmiennej r do bufora znakowego, tworząc łańcuch określający nazwę pliku:
   #include <stdio.h>
   ...
   void main()
   {
      int r, m;
      char nazwa[20];
      printf("Rok = ");
      scanf("%d", &r);
      sprintf(nazwa, "%d.txt", r);
      FILE *plik = fopen(nazwa, "wt");
      for (m = 1; m<=12; m++)
      {
         ...
      }
      fclose(plik);
   }
Druga wersja tego programu, oparta całkowicie na mechanizmach strumieni wejścia-wyjścia języka C++, używa obiektu klasy ostrstream związanego z buforem znakowym. Za pomocą operatora << do bufora wypisana zostaje wartość zmiennej r, łańcuch .txt i znak \0 oznaczający koniec formatowanego napisu określającego nazwę pliku (zamiast znaku \0 można użyć manipulatora ends):
   #include <fstream.h>
   #include <iostream.h>
   #include <strstream.h>
   ...
   void main()
   {
      int r, m;
      cout << "Rok = ";
      cin >> r;
      char nazwa[20];
      ostrstream nstr(nazwa, sizeof(nazwa));
      nstr << r << ".txt" << '\0';
      ofstream plik(nazwa);
      for (m = 1; m<=12; m++)
      {
         ...
      }
   }
Okazuje się, że ta wersja daje nieprawidłowy wynik, gdy zostanie skompilowana za pomocą kompilatora Borland C++ 5.5. Przykładowo, po wczytaniu liczby 2006 tworzy plik o nazwie 1792.txt, w którym zapisuje kalendarz na rok 1792 zamiast 2006. Jeśli natomiast zostanie skompilowana w środowisku Borland C++ 3.1, Borland C++ 4.52, Dev-C++ lub Visual C++ 2005 Express Edition, działa poprawnie (w przypadku dwóch ostatnich kompilatorów wymagane są drobne zmiany kodu). Testy programu wykazują, że konstruktor obiektu nstr klasy ostrstream zeruje bardziej znaczący bajt zmiennej r. Dlatego wartość 2006, której reprezentacją dwójkową jest ciąg bitów 11111010110, zostaje nieoczekiwanie zamieniona na wartość 1792 reprezentowaną dwójkowo przez ciąg 11100000000. Ten ewidentny błąd kompilatora można ominąć, określając mniejszy rozmiar bufora:
   ...
   char nazwa[20];
   ostrstream nstr(nazwa, sizeof(nazwa) - 1);
   ...
Natomiast poprawnie działa inna wersja programu, w której strumień do przygotowania nazwy pliku jest konstruowany bez podawania własnego bufora. Strumień jest wówczas związany z buforem klasy streambuf utworzonym automatycznie na stercie. Dostęp do zapisanego w strumieniu napisu umożliwia funkcja składowa str() klasy streambuf:
   #include <fstream.h>
   #include <iostream.h>
   #include <strstream.h>
   ...
   void main()
   {
      int r, m;
      cout << "Rok = ";
      cin >> r;
      ostrstream nstr;
      nstr << r << ".txt" << '\0';
      ofstream plik(nstr.str());
      for (m = 1; m<=12; m++)
      {
         ...
      }
      delete[] nstr.str();
   }
Wywołanie funkcji str() powoduje "zamrożenie" strumienia nstr, dlatego pamięć, w której przygotowany został napis reprezentujący nazwę pliku, jest zwalniana za pomocą operatora detete[]. Gdyby tego nie zrobić, wystąpiłoby zjawisko "wycieku pamięci" (ang. memory leak) - program po zakończeniu wykonania zostałby usunięty z pamięci, ale pamięć przydzielona buforowi klasy streambuf nie zostałaby zwolniona.

Warto przy okazji wspomnieć, że w nowszym standardzie języka C++ zaleca się korzystanie z biblioteki stringstream zamiast strstream uważanej za przestarzałą. W najnowszych środowiskach programowania C++ biblioteka strstream nawet nie jest definiowana. Oto działająca poprawnie wersja rozpatrywanego programu zbudowanego w oparciu o klasę ostringstream:

   #include <fstream.h>
   #include <iostream.h>
   #include <sstream.h>
   ...
   void main()
   {
      int r, m;
      cout << "Rok = ";
      cin >> r;
      ostringstream nstr;
      nstr << r << ".txt" << '\0';
      ofstream plik(nstr.str().c_str());
      for (m = 1; m<=12; m++)
      {
         ...
      }
   }
Funkcja składowa str() klasy ostringstream udostępnia bufor, który jest łańcuchem C++ - obiektem klasy string. Z kolei funkcja składowa c_str() klasy string pozwala na potraktowanie tego łańcucha C++ jako łańcucha w stylu języka C (ciąg znaków zakończony znakiem \0).

Przekazywanie wartości zmiennopozycyjnej funkcji
Zapewne każdy programista zgodzi się z tym, że poniższy program wypisuje nazwiska i imiona wszystkich uczniów zarejestrowanych w pliku o nazwie Oceny.dta, którzy uzyskali najwyższą średnią ocenę z sześciu przedmiotów. Program czyta plik dwukrotnie: pierwszy raz, by znależć najwyższą średnią ocenę, drugi raz, by wypisać dane uczniów, którzy tę średnią osiągnęli:
   #include <stdio.h>
   #include <conio.h>

   struct UCZEN
   {
      char  imie[13];
      char  nazwisko[21];
      char  plec;
      char  klasa[3];
      short polski;
      short matma;
      short historia;
      short gegra;
      short fizyka;
      short chemia;
   };

   double srednia(UCZEN &rec)
   {
      return (rec.polski + rec.matma + rec.historia +
              rec.gegra + rec.fizyka + rec.chemia)/6.0;
   }

   void main()
   {
      FILE *plik = fopen("Oceny.dta", "rb");
      UCZEN rec;
      double srMax = 0, sr;
      while (fread(&rec, sizeof(rec), 1, plik) == 1)
         if ((sr = srednia(rec)) > srMax) srMax = sr;
      fseek(plik, 0, SEEK_SET);
      printf("Najlepsi (srednia %lf):\n----------------------------\n", srMax);
      while (fread(&rec, sizeof(rec), 1, plik) == 1)
         if (srednia(rec) == srMax)
            printf("%s %s\n", rec.nazwisko, rec.imie);
      fclose(plik);
      getch();
   }
Program nie działa jednak poprawnie, gdy zostanie skompilowany za pomocą kompilatorów Borland C++ (sprawdzono wersje 3.1, 4.52, 5.5, Builder 6, Turbo C++ Explorer, Builder 2006). Mianowicie, dla przykładowego pliku zawierającego 61 rekordów nie wypisze ani jednego nazwiska i imienia. Gdy jednak zostanie skompilowany np. za pomocą kompilatora Dev-C++ lub Visual C++ 2005 Express Edition, zachowuje się właściwie, wypisując nazwiska i imiona dwóch uczniów, którzy osiągnęli maksymalną średnią. Ciekawostką jest to, że da prawidłowy wynik przy użyciu kompilatora Borland C++, gdy druga pętla zostanie zapisana nieoptymalnie:
   ...
   while (fread(&rec, sizeof(rec), 1, plik) == 1)
   {
      sr = srednia(rec);
      if (sr == srMax)
         printf("%s %s\n", rec.nazwisko, rec.imie);
   }
   ...
Czym wytłumaczyć to zaskakujące zjawisko? Problem tkwi w tym, że obliczenia zmiennopozycyjne są wykonywane przez koprocesor arytmetyczny na wartościach 10-bajtowych, tj. typu long double. Kompilatory Borland C++ nie dokonują jednak konwersji tak obliczonej wartości do wartości 8-bajtowej, tj. typu double, pomimo że w definicji funkcji określono, że ma ona zwracać wartość typu double. Ten błąd kompilatorów Borland C++ można łatwo naprawić, wymuszając taką konwersję poprzez użycie dodatkowej zmiennej lokalnej:
   double srednia(UCZEN &rec)
   {
      double s = (rec.polski + rec.matma + rec.historia +
                  rec.gegra + rec.fizyka + rec.chemia)/6.0;
      return s;
   }

A oto przykład prostszego programu, który ujawnia ten sam błąd:
   #include <iostream.h>

   double dist(double a, double b)
   {
      return sqrt(a*a + b*b);
   }

   void main()
   {
      double x[] = {1.2, 0.5, 2.8},
             y[] = {2.8, 1.3, 1.2}, dMax = 0, d;
      for (int k=0; k<3; k++)
         if ((d = dist(x[k], y[k])) > dMax) dMax = d;
      for (int k=0; k<3; k++)
         if (dist(x[k], y[k]) == dMax)
            cout << x[k] << " " << y[k] << endl;
   }
Program znajduje i wypisuje na monitorze współrzędne tych punktów spośród (1.2, 2.8), (0.5, 1.3) i (2.8, 1.2), które są najbardziej odległe od początku układu na płaszczyźnie. Algorytm polega na dwukrotnym przeglądaniu tablic współrzędnych punktów: pierwszy raz, by znaleźć największą odległość punktu od początku układu, drugi raz, by wyszukać te punkty, które tę odległość osiągają. Ten program również nie wypisze żadnego wyniku, gdy zostanie skompilowany za pomocą kompilatora Borland C++, a znajdzie dwa punkty, gdy do jego kompilacji zostanie użyty inny kompilator. Błędu można uniknąć, definiując funkcję dist() następująco:
   double dist(double a, double b)
   {
      double d = sqrt(a*a + b*b);
      return d;
   }
Wniosek: Dobrym zwyczajem programowania jest wymuszanie konwersji obliczonej przez koprocesor wartości bardziej dokładnego typu zmiennopozycyjnego (long double) na wartość mniej dokładnego typu zmiennopozycyjnego (double lub float) określonego w definicji funkcji poprzez użycie zmiennej lokalnej i przekazanie w instrukcji return wartości tej zmiennej jako wyniku funkcji.


Powrót do początku