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