PROGRAMOWANIE – PRZYKŁADY
IDE C++ WinBGI C# Wpadki

Wpadki programistyczne

Konwersja numeru roku na nazwę pliku Przekazywanie wartości zmiennopozycyjnej funkcji Potencjalnie niechciany program Konwersja wartości zmiennopozycyjnej na całkowitą Przeciążanie funkcji standardowej abs Kontakt

Konwersja numeru roku na nazwę pliku

Kompilator Borland C++ 5.5

W programie drukowania kalendarza do pliku tekstowego, zaprezen­towanym w drugim wydaniu książki Turbo Pascal i Borland C++. Przykłady (Helion 2006, s. 62–63), wczytana z klawia­tury liczba całkowita r reprezen­tująca numer roku jest zamie­niana na nazwę pliku postaci RRRR.TXT. Pierwsza wersja programu w języku C zawiera funkcję biblio­teczną sprintf, która wywołana z symbo­lem formato­wania %d.txt wypro­wadza wartość zmiennej r do bufora znakowego, tworząc łańcuch określa­ją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 (s. 65–66), oparta całkowicie na mecha­nizmach strumieni wejścia-wyjścia języka C++, używa obiektu klasy ostrstream związa­nego z buforem znakowym. Za pomocą opera­tora << do bufora wypisana zostaje wartość zmiennej r, łańcuch .txt i znak \0 oznacza­jący koniec formato­wanego napisu określa­jącego nazwę pliku (zamiast znaku \0 można użyć manipu­latora 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++)
    {
        ...
    }
}

Pełny kod źródłowy i projekt programu do pobrania dla różnych kompi­latorów:

Okazuje się, że ta wersja daje nieprawidłowy wynik, gdy zostanie skompi­lowana za pomocą kompila­tora Borland C++ 5.5. Przykła­dowo, po wczy­taniu liczby 2006 tworzy plik o nazwie 1792.txt, w którym zapisuje kalendarz na rok 1792 zamiast 2006. Jeśli nato­miast zostanie skompi­lowana w środo­wisku Borland C++ 3.1, Borland C++ 4.52, MinGW C++ lub Visual C++ 2005 Express Edition, działa popra­wnie (w przy­padku dwóch ostatnich kompila­torów wymagane są drobne zmiany kodu). Testy wykazują, że konstru­ktor obiektu nstr klasy ostrstream zeruje pierwszy bajt pamięci przydzie­lonej zmiennej r, czyli osiem najmniej znaczą­cych bitów w matema­tycznym zapisie dwójkowym liczby (rosnący porządek bajtów, ang. little-endian). Dlatego wartość 2006, której reprezen­tacją dwójkową jest 11111010110, zostaje nieoczeki­wanie zamieniona na wartość 1792 reprezen­towaną dwójkowo przez ciąg 11100000000. Ten ewidentny błąd kompila­tora można ominąć, określa­jąc mniejszy rozmiar bufora:

...
char nazwa[20];
ostrstream nstr(nazwa, sizeof(nazwa) - 1);
...

Z kolei poprawnie działa trzecia wersja programu, w której strumień nstr służący do przygoto­wania nazwy pliku jest konstru­owany bez podawania własnego bufora. Strumień jest wówczas wiązany z buforem klasy streambuf utwo­rzonym automa­tycznie na stercie. Napis zapisany w stru­mieniu udostę­pnia funkcja składowa str klasy ostrstream:

#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 stru­mienia nstr, dlatego pamięć, w której przygo­towany został napis reprezen­tujący nazwę pliku, jest zwalniana za pomocą opera­tora 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ęć przydzie­lona buforowi klasy streambuf nie zosta­łaby zwolniona.

Warto przy okazji wspomnieć, że w nowszym standardzie języka C++ zaleca się korzy­stanie z klasy stringstream i jej pocho­dnych zamiast strstream uważanej za przesta­rzałą. Oto działa­jąca poprawnie wersja rozpatry­wanego programu zbudo­wanego 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ńcu­chem C++ – obiektem klasy string. Z kolei funkcja składowa c_str klasy string pozwala na potrakto­wanie 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

Kompilatory Borland C++ i MinGW C++

Zdarzyło się to na ćwiczeniach z programowania w językach C i C++. Studenci mieli opra­cować program, który przeszu­kuje plik binarny Oceny.dta zawiera­jący podsta­wowe dane o pewnej grupie uczniów (61 osób) i wypi­suje nazwiska i imiona tych z nich, którzy uzyskali najwyższą średnią ocenę z sześciu przed­miotów. Ze względu na prostotę zadania nie było potrzeby wcześniej­szego przygoto­wania i przete­stowania programu. Algorytm polegał na dwukro­tnym przeglą­daniu pliku: pierwszy raz, by znależć najwyższą średnią ocenę, drugi raz, by wypisać nazwiska i imiona uczniów, którzy tę średnią osiągnęli. Po kilku próbach powstał mniej więcej taki program:

#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;
}

int 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();
    return 0;
}

Pełny kod źródłowy i projekt programu do pobrania dla różnych kompilatorów:

Ku zaskoczeniu niektórych studentów, a zwłaszcza mojemu, program wypi­sywał najlepszą średnią ocenę, lecz ani jednego nazwiska i imienia. Analiza kodu źródło­wego prowa­dziła do wniosku, że program jest poprawny, a ponieważ czas na ćwicze­niach biegnie o wiele szybciej niż normalnie, pozostało poprosić o zwłokę na wyjaśnienie dziwnego zacho­wania się programu i przejść do innego zadania.

Po wielu próbach "znęcania się" nad kodem źródłowym okazało się, że wina leży po stronie kompila­tora Borland C++ 5.5. Źle sprawują się również inne kompila­tory Borland (np. wersje 3.1, 4.52, Builder 6, Turbo C++ Explorer i Builder 2006), a także MinGW (np. MinGW C++ 5.1.0). Gdy program zostanie skompi­lowany za pomocą kompila­torów Microsoft Visual C++ (np. Express Edition 2005 i Community 2017), zacho­wuje się poprawnie, wypi­sując nazwiska i imiona dwóch uczniów, którzy osiągnęli najwyższą średnią. Program daje prawi­dłowy wynik również przy użyciu kompila­torów "winowajców", gdy druga pętla zostanie zapisana nieopty­malnie:

...
while (fread(&rec, sizeof(rec), 1, plik) == 1)
{
    sr = srednia(rec);
    if (sr == srMax)
        printf("%s %s\n", rec.nazwisko, rec.imie);
}
...

Ta modyfikacja ujawnia, że źródło błędu tkwi w kodzie wyni­kowym funkcji srednia. Otóż obliczenia zmiennopo­zycyjne są wykony­wane przez kopro­cesor arytme­tyczny na warto­ściach 10-bajtowych, czyli typu long double. Kompila­tory Borland C++ i MinGW C++ nie doko­nują jednak konwersji tak obliczonej wartości 10-bajtowej do wartości 8-bajtowej typu double. Oznacza to, że funkcja srednia zwraca wartości typu long double, pomimo że w jej defi­nicji określono, że mają to być wartości typu double. Ten błąd kompila­torów można łatwo naprawić, wymu­szając taką konwersję poprzez użycie dodat­kowej 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>
#include <cmath>

using namespace std;

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

int 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;
    return 0;
}

Kod źródłowy i projekt programu do pobrania dla różnych kompilatorów:

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ą najbar­dziej odległe od początku układu współ­rzędnych na płaszczy­źnie. Algorytm polega na dwukro­tnym przeglą­daniu tablic współ­rzędnych punktów: pierwszy raz, by wyzna­czyć największą odle­głość punktu od początku układu, drugi raz, by wyszukać te punkty, które tę odle­głość osiągają. Program nie znajduje żadnego punktu, gdy zostanie skompi­lowany za pomocą kompila­tora Borland C++, a znajduje dwa, gdy do jego kompi­lacji zostanie użyty kompi­lator MinGW C++ lub Visual C++. Tym razem dobry wynik uzyskany za pomocą kompila­tora MinGW C++ zaska­kuje pozy­tywnie (być może wpływ ma algorytm obli­czania wartości funkcji sinus). Błędu kompila­tora Borland C++ można uniknąć, defi­niując funkcję dist następująco:

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

Wniosek: Dobrym zwyczajem progra­mowania jest wymuszanie konwersji obli­czonej przez kopro­cesor wartości bardziej dokła­dnego typu zmiennopo­zycyjnego (long double) na wartość mniej dokła­dnego typu zmiennopo­zycyjnego (double lub float) określo­nego w defi­nicji funkcji poprzez użycie zmiennej lokalnej i przeka­zanie w instru­kcji return wartości tej zmiennej jako wyniku funkcji. A może lepiej używać pewniej­szych narzędzi?

Potencjalnie niechciany program

AVG Free Edition 9.0

Pewnego razu studentka informatyki zwróciła mi uwagę, że załą­czony do książki Wprowa­dzenie do algo­rytmów i struktur danych (Polite­chnika Radomska 2005, 2007) przykła­dowy program Project1.exe (dodatek, podka­talog Pierwszy) jest zawiru­sowany. Wydawało mi się to nieprawdo­podobne, bo dbam o mój komputer, ale wyklu­czyć infekcji programu nie mogłem, bo nawet tzw. pełna ochrona systemu kompute­rowego jego pełnej ochrony nie gwaran­tuje. Dlaczego jednak wszystkie inne programy załą­czone do książki nie są wykazy­wane jako niebez­pieczne?

Program jest bardzo prostą aplikacją okienkową zbudowaną w środo­wisku Borland Delphi 7. Na formu­larzu (rys. powyżej) stano­wiącym projekt okna głównego aplikacji umieszczono tylko jeden kompo­nent – przycisk Button1 o ety­kiecie Zamknij, króremu przypi­sano proce­durę obsługi zdarzenia OnClick powodu­jącą zamknię­cie okna urucho­mionej aplikacji i zakoń­czenie jej wykonania:

procedure TForm1.Button1Click(Sender: TObject);
begin
    Close;
end;

Kompilacja wersji źródłowej tego programu prowadzi do wersji wyko­nawczej, która przez program antywi­rusowy AVG Free Edition 9.0 trakto­wana jest jako poten­cjalnie niechciany program typu Adware Generic. Wystarczy jednak umieścić na formu­larzu np. komponent Label, a program nie będzie trakto­wany jako niepo­żądany. Wynika stąd, że być może Delphi 7 generuje dziwny kod wyko­nawczy tak prostej aplikacji, ale raczej, że niektóre programy antywi­rusowe są nadgorliwe w wyszu­kiwaniu niebezpie­cznego oprogra­mowania.

Konwersja wartości zmiennopozycyjnej na całkowitą

Kompilatory Borland C++ i MinGW C++

W programie wydawania reszty pojawił się problem konwersji wczyty­wanej z klawia­tury nieu­jemnej liczby rzeczy­wistej wyraża­jącej kwotę w złotych (część całkowita) i groszach (część ułamkowa) na liczbę całko­witą reprezen­tującą tę kwotę w groszach. Ogólnie wiadomo, że działania na liczbach zmiennopozy­cyjnych mogą nie być dokładne, a nawet już sam zapis takiej liczby może pociągać utratę dokła­dności. Oczywistym jest więc, że pomno­żona przez 100 wartość zmiennej Reszta typu double przekształ­cona na wartość zmiennej Wynik typu int za pomocą instrukcji

Wynik = 100 * Reszta;

może być obarczona błędem. Oto kilka przykła­dowych wyników uzyska­nych przy użyciu trzech kompila­torów ujawnia­jących to zjawisko:

Reszta Borland C++ MinGW C++ Visual C++
0.09 8 8 9
2.78 277 277 278
145.74 14574 14574 14574
148.76 14875 14875 14876
222.22 22221 22221 22222

Jak widać, w przypadku kompilatorów Borland C++ i MinGW C++ niezbędna jest korekta wartości rzeczy­wistej przed jej konwersją na wartość całko­witą, jak np.:

Wynik = 100 * Reszta + 0.5;

Chociaż kompilator Visual C++ pozytywnie zaskakuje, nadal może wydawać się rozsądnym użycie skorygo­wanej instru­kcji przypisania.

W ogólnym przypadku, gdy zmienna Reszta może przyjmo­wać wartości rzeczy­wiste dowol­nego znaku reprezen­tujące kwotę złotowo-groszową, przekształ­cenie jej na kwotę groszową można zaprogra­mować następująco:

if (Reszta >= 0)
    Wynik = 100 * Reszta + 0.5;
else
    Wynik = 100 * Reszta - 0.5;

lub prościej z użyciem wyrażenia warunkowego:

Wynik = 100 * Reszta + ((Reszta >= 0) ? 0.5 : -0.5);

Przeciążanie funkcji standardowej abs

Kompilator Borland C++ 5.5

W języku C++ funkcja standardowa abs z biblio­teki cmath jest przecią­żona, m.in. zwraca wartości bezwzględne typu int dla argu­mentów typu int i warto­ści bezwzglę­dne typu double dla argu­mentów typu double. Zatem program

#include <iostream>
#include <cmath>

using namespace std;

int main()
{
    double x = -2.75;
    cout << "abs(" << x << ") = " << abs(x) << endl;
    return 0;
}

powinien wypisać w oknie konsoli wynik

abs(-2.75) = 2.75

Okazuje się, że w przypadku kompilatora Borland C++ 5.5 funkcja abs działa "w duchu" języka C (rys.), miano­wicie zwraca wartości typu int, doko­nując w razie potrzeby konwersji argu­mentów na typ int. Dla argu­mentów typu double należy użyć funkcji fabs, by otrzymać prawi­dłowy wynik.


© 2001-2010 by Kazimierz Jakubczyk
Reorganizacja witryny: czerwiec 2018