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

Przykład C++

Kalendarz roczny Pliki w języku C Program w Borland C++ Program w MinGW C++ Program w Visual C++ Strumienie w C++ Program w C++ Poprzedni przykład Następny przykład Program w Visual C# Kontakt

Kalendarz roczny

Zadaniem programu jest wyprowadzenie kalen­darza rocznego do pliku teksto­wego. Zakładamy, że nazwa pliku jest łańcu­chem postaci RRRR.txt, w którym RRRR jest zapisem liczby całko­witej wczytanej z klawia­tury i określa­jącej numer roku, dla którego należy zbudować kalen­darz. Program powinien utworzyć 12 bloków tekstu reprezen­tujących kolejne miesiące roku w układzie podobnym do wyświe­tlanego na ekranie kalen­darza miesię­cznego i zapisać je w pliku.

Pliki w języku C

Biblioteka standardowa języka C udostępnia zestaw funkcji realizu­jących komuni­kację programu z plikami i formato­wanie tekstu. Przed czytaniem danych z pliku lub zapisem danych do pliku należy go najpierw otworzyć za pomocą dwuargu­mentowej funkcji fopen. Oba jej argu­menty są łańcu­chami. Pierwszy określa nazwę pliku (wraz z ew. ścieżką dostępu), drugi infor­muje o trybie dostępu do pliku:

r otwarcie istniejącego pliku do czytania
w utworzenie pliku do pisania (jeśli plik istnieje, jego zawartość zostaje skasowana)
a otwarcie pliku do dopisywania na końcu (jeśli plik nie istnieje, zostaje utworzony)
r+ otwarcie istniejącego pliku do aktualizacji (dozwolony zapis i odczyt)
w+ utworzenie pliku do aktualizacji (jeśli plik istnieje, jego zawartość zostaje skasowana)
a+ otwarcie pliku do aktualizacji z dopisywaniem na końcu (jeśli plik nie istnieje, zostaje utworzony)
t plik tekstowy (wartość domyślna, gdy nie określono t ani b)
b plik binarny

Wartością funkcji fopen jest wskaźnik na strukturę typu FILE zawiera­jącą infor­macje o otwartym pliku, a przy­padku nieuda­nego otwarcia wartość NULL (wskaźnik pusty). Po pomyślnym otwarciu pliku można reali­zować operacje transferu danych pomiędzy programem a plikiem, używając stosownych funkcji odwołu­jących się do pliku za pomocą wskaźnika dostar­czonego przez funkcję fopen. Na koniec należy zamknąć plik za pomocą funkcji fclose, której jedynym argu­mentem jest wskaźnik na plik. Funkcja zwraca wartość 0, gdy operacja zamykania pliku powiodła się, lub wartość EOF (end of file, koniec pliku), gdy wystąpił błąd. Defi­nicje struktury FILE oraz stałych NULLEOF znajdują się w pliku nagłów­kowym stdio.h.

Zazwyczaj przy formatowanym czytaniu danych z pliku i pisaniu do pliku korzysta się z funkcji fscanffprintf, które działają podobnie jak funkcje scanfprintf. Ich dodatkowym, pierwszym argu­mentem jest wskaźnik na plik. Funkcja fprintf zwraca liczbę bajtów wyprowadzonych do pliku, a przy­padku błędu wartość ujemną, natomiast funkcja fscanf zwraca liczbę wczytanych pól, a w przy­padku błędu – zero lub znacznik końca pliku EOF. Poniższy fragment programu pobiera z pliku teksto­wego o nazwie Dane.txt współ­rzędne punktów płaszczyzny do dwóch tablic dynami­cznych typu double. Czytany plik zawiera w pierwszym wierszu liczbę wszystkich punktów, zaś w nastę­pnych wierszach po dwie współ­rzędne kolejnych punktów oddzie­lone co najmniej jedną spacją.

FILE *plik = fopen("Dane.txt", "rt");
int n;
fscanf(plik, "%d", &n);
double *x = new double[n];
double *y = new double[n];
for (int i = 0; i < n; i++)
    fscanf(plik, "%lf %lf", x+i, y+i)
fclose(plik);
...
delete[] x;
delete[] y;

Argumentami funkcji fscanf, poczynając od trzeciego, są, podobnie jak w przy­padku scanf, wskaźniki na zmienne (adresy obszarów pamięci zajmo­wanych przez zmienne), których wartości mają być wczytane. Dlatego w powyż­szych jej wywoła­niach są nimi wyrażenia &n oraz x+iy+i. Przypo­mnijmy, że dwa ostatnie wyra­żenia można zapisać w równo­ważnej postaci &x[i]&y[i]. Ich warto­ściami są wskaźniki na elementy tablic.

Standardowa biblioteka wejścia-wyjścia języka C zawiera wiele innych funkcji doty­czących operacji plikowych, m.in. pobierania i zapisu pojedyn­czego znaku, czytania wiersza tekstu i zapisu łańcucha znaków oraz czytania i zapisu bloków bajtów. Na uwagę zasłu­gują również dwie funkcje o nazwach sscanfsprintf, których pierwszym argu­mentem nie jest wskaźnik identyfi­kujący plik jak w przy­padku funkcji fscanffprintf, lecz tablica znaków. Pierwsza funkcja zamiast z pliku pobiera znaki z tablicy i interpre­tuje je zgodnie z zadanym formatem, zaś druga formatuje określone wartości i kieruje tekst wynikowy do tablicy, która powinna być dostate­cznie duża, by pomieścić wynik. Jeśli np. s jest tablicą co najmniej 11-znakową, a d, mr są zmiennymi całko­witymi reprezen­tującymi datę, to instrukcja

sprintf(s, "%02d.%02d.%04d", d, m, r);

wypisuje datę według normy polskiej (DD.MM.RRRR), ale wynik umieszcza w łańcu­chu s. Zero występu­jące w symbolu formato­wania po znaku % oznacza, że wyprowa­dzany ciąg znaków ma być poprze­dzony znakami 0 do pełnej szero­kości pola, gdy jest za krótki. Z kolei instrukcja

sscanf(s, "%d.%d.%d", &d, &m, &r);

wczytuje z łańcucha s zawierającego datę trzy liczby całkowite do zmiennych d, mr. Kropka po symbolu formato­wania oznacza, że jest oczekiwana na wejściu, czyli w tym przypadku jest znakiem kończącym zapis liczby całkowitej.

Program w Borland C++

Pełny kod źródłowy programu w języku Borland C++ wyprowadza­jącego kalen­darz roczny do pliku tekstowego jest przedsta­wiony poniżej. Nazwa pliku została uzyskana w funkcji main za pomocą funkcji sprintf w wyniku konwersji wczyta­nego z klawia­tury numeru roku na łańcuch postaci RRRR.txt.

#include <stdio.h>
#include <string.h>
#include "kalend.h"

char *Mc[] = {"",
     "Styczen", "Luty", "Marzec", "Kwiecien", "Maj", "Czerwiec", "Lipiec",
     "Sierpien", "Wrzesien", "Pazdziernik", "Listopad", "Grudzien"};

void miesiac(int m, int r, FILE *plik)
{
    fprintf(plik,
            "%*s%s %d\n"
            " --------------------\n Nd Po Wt Sr Cz Pt So\n"
            " --------------------\n",
            (17 - strlen(Mc[m])) / 2, "", Mc[m], r);
    int n = dtyg(1, m, r), max = dmax(m, r);
    if (n > 0)
        fprintf(plik, "%*s", 3*n, "");
    for (int d = 1; d <= max; d++)
    {
        fprintf(plik, "%3d", d);
        if ((n + d) % 7 == 0)
            fprintf(plik, "\n");
    }
    if ((n + max) % 7)
        fprintf(plik, "\n");
}

int main()
{
    int r;
    char nazwa[20];
    printf("Rok = ");
    scanf("%d", &r);
    sprintf(nazwa, "%d.txt", r);
    FILE *plik = fopen(nazwa, "wt");
    for (int m = 1; m <= 12; m++)
    {
        miesiac(m, r, plik);
        if (m < 12)
            fprintf(plik, "\n");
    }
    fclose(plik);
    return 0;
}

Na uwagę zasługuje sposób wyprowa­dzania zmiennej liczby spacji w funkcji miesiac. Polega on na użyciu formatu %*s, w którym znak * oznacza, że infor­macja o szero­kości pola ma być pobrana z argu­mentu typu int poprzedza­jącego druko­waną wartość (łańcuch pusty):

fprintf(plik, "%*s", szerokość_pola, "");

Operacja wypisywania zmiennej liczby spacji jest wykony­wana dwukro­tnie – pierwszy raz, by wycen­trować nagłówek z nazwą miesiąca i numerem roku, drugi raz, by przesunąć jedynkę określa­jącą dzień rozpoczy­nający miesiąc, gdy nie przypada on w niedzielę.

Plik kalendarza wygenerowany przez program jest kodowany w podsta­wowym kodzie ASCII, toteż polskie litery są w nim reprezen­towane przez stosowne litery alfabetu łacińskiego. Poniższe łącze udostę­pnia przykła­dowy plik wynikowy programu w kodzie ASCII.

Program w MinGW C++

Kod źródłowy programu tworzącego kalendarz roczny w pliku tekstowym w środo­wisku MinGW C++ i korzysta­jącego z biblioteki standar­dowej stdio jest niemal identy­czny jak jego odpowie­dnik w Borland C++. Istotna różnica polega na użyciu modyfi­katora const w defi­nicji tablicy Mc. Jego brak stałby się, podobnie jak w programie zapisu liczby w notacji rzymskiej, źródłem szeregu ostrzeżeń kompila­tora informu­jących o niezgo­dności typu char* elementów tablicy z typem const char* stałych łańcu­chowych je inicjali­zujących.

#include <stdio.h>
#include <string.h>
#include "kalend.h"

const char *Mc[] = {"",
    "Stycze\xf1", "Luty", "Marzec", "Kwiecie\xf1", "Maj", "Czerwiec", "Lipiec",
    "Sierpie\xf1", "Wrzesie\xf1", "Pa\x9f" "dziernik", "Listopad", "Grudzie\xf1"};

void miesiac(int m, int r, FILE *plik)
{
    fprintf(plik,
            "%*s%s %d\n"
            " --------------------\n Nd Po Wt \x8cr Cz Pt So\n"
            " --------------------\n",
            (17 - strlen(Mc[m])) / 2, "", Mc[m], r);
    int n = dtyg(1, m, r), max = dmax(m, r);
    if (n > 0)
        fprintf(plik, "%*s", 3*n, "");
    for (int d = 1; d <= max; d++)
    {
        fprintf(plik, "%3d", d);
        if ((n + d) % 7 == 0)
            fprintf(plik, "\n");
    }
    if ((n + max) % 7)
        fprintf(plik, "\n");
}

int main()
{
    int r;
    char nazwa[20];
    printf("Rok = ");
    scanf("%d", &r);
    sprintf(nazwa, "%d.txt", r);
    FILE *plik = fopen(nazwa, "wt");
    for (int m = 1; m <= 12; m++)
    {
        miesiac(m, r, plik);
        if (m < 12)
            fprintf(plik, "\n");
    }
    fclose(plik);
    return 0;
}

Program dodatkowo uwzględnia polskie litery, zapisując je w pliku zgodnie z normą Windows-1250. I tak np. litera ń ma w tym standar­dzie kod szesna­stkowy 0xf1 (dzie­siętny 241), toteż jest w pięciu łańcu­chach nazw miesięcy reprezen­towana przez znak '\xf1': "Stycze\xf1", "Kwiecie\xf1" itd. Łańcuch "Pa\x9fdziernik" został podzie­lony na dwa podłań­cuchy, by do zapisu szesna­stkowego '\x9f' reprezen­tującego literę ź (kod dziesiętny 159) nie została dołączona litera d, gdyż w prze­ciwnym razie byłaby potrakto­wana jako cyfra szesna­stkowa, przez co zapis '\x9fd' zostałby zinter­pretowany jako znak o kodzie dziesię­tnym 2557 przekra­czającym zakres typu char. Poniższe łącze udostę­pnia przykła­dowy plik wynikowy programu w kodzie Windows-1250.

Program w Visual C++

Wydaje się naturalne, że kod źródłowy programu wyprowadza­jącego kalendarz roczny do pliku teksto­wego powinien dać się przenieść bez kłopo­tliwych zmian ze środo­wiska MinGW C++ do Visual C++. Oczywistą modyfi­kacją jest zastą­pienie przesta­rzałej w nowym środo­wisku funkcji scanf funkcją scanf_s, która była używana w kilku przyto­czonych wcześniej przykła­dowych programach. To jednak nie wystarcza – próba kompi­lacji programu kończy się niepowo­dzeniem, gdyż kompi­lator Visual C++ traktuje także funkcje sprintffopen jako przesta­rzałe i poten­cjalnie niebez­pieczne. Można oczy­wiście wymusić ich użycie, wyłączając zabezpie­czenia przed stoso­waniem przesta­rzałych funkcji lub zmiennych, ale lepszym rozwią­zaniem jest skorzy­stanie z ich nowszych odpowie­dników sprintf_sfopen_s.

Funkcja sprintf_s ma w porównaniu z sprintf dodatkowy, drugi argument określa­jący maksymalną liczbę znaków do zapisania w tablicy znakowej. Również funkcja fopen_s ma dodatkowy argument, ale na pierwszym miejscu listy. Jego typem jest FILE** – wskaźnik na wskaźnik na strukturę typu FILE. Jeśli np. zmienna plik byłaby typu FILE*, to pierwszym argu­mentem wywo­łania fopen_s byłoby wyra­żenie &plik (wskaźnik na zmienną plik). Funkcja zwraca zero w przy­padku powo­dzenia, a nieze­rowy kod błędu w prze­ciwnym razie. Uwzglę­dnienie tych modyfi­kacji w funkcji main prowadzi do następu­jącego kodu źródło­wego programu:

#include <stdio.h>
#include <string.h>
#include "kalend.h"

const char *Mc[] = {"",
    "Stycze\xf1", "Luty", "Marzec", "Kwiecie\xf1", "Maj", "Czerwiec", "Lipiec",
    "Sierpie\xf1", "Wrzesie\xf1", "Pa\x9f" "dziernik", "Listopad", "Grudzie\xf1"};

void miesiac(int m, int r, FILE *plik)
{
    fprintf(plik,
            "%*s%s %d\n"
            " --------------------\n Nd Po Wt \x8cr Cz Pt So\n"
            " --------------------\n",
            (17 - strlen(Mc[m])) / 2, "", Mc[m], r);
    int n = dtyg(1, m, r), max = dmax(m, r);
    if (n > 0)
        fprintf(plik, "%*s", 3*n, "");
    for (int d = 1; d <= max; d++)
    {
        fprintf(plik, "%3d", d);
        if ((n + d) % 7 == 0)
            fprintf(plik, "\n");
    }
    if ((n + max) % 7)
        fprintf(plik, "\n");
}

int main()
{
    int r;
    char nazwa[20];
    printf("Rok = ");
    scanf_s("%d", &r);
    sprintf_s(nazwa, sizeof(nazwa), "%d.txt", r);
    FILE *plik;
    fopen_s(&plik, nazwa, "wt");
    for (int m = 1; m <= 12; m++)
    {
        miesiac(m, r, plik);
        if (m < 12)
            fprintf(plik, "\n");
    }
    fclose(plik);
    return 0;
}

Strumienie w C++

W środowisku C++ istnieje obszerna biblioteka stru­mieni wejścia-wyjścia dostar­czająca elasty­cznych i efekty­wnych mecha­nizmów przesy­łania danych i obsługi plików. Strumienie można kojarzyć z klawia­turą, monitorem, plikiem lub buforem pamięci (np. tablicą znaków). Ich przykła­dami są obiekty cincout zdefinio­wane w standar­dowej biblio­tece iostream i używane w kilku przyto­czonych wcześniej przykła­dowych progra­mach. Operacje czytania z klawia­tury i pisania na ekranie monitora są w nich realizo­wane za pomocą opera­torów >><<, a forma­towanie wyjścia za pomocą manipu­latorów. Na przykład szero­kość pola określa manipu­lator setw z biblio­teki iomanip. Narzędzia te są dostępne dla strumieni związanych z plikami tekstowymi i buforem znaków.

Plik można otworzyć po uprzednim dołączeniu do programu biblio­teki fstream za pomocą dyrektywy #include, definiując strumień typu ifstream, ofstream lub fstream w zależności od tego, czy ma on służyć do wejścia, wyjścia, czy też zarówno do wejścia, jak i do wyjścia. Na przykład definicja

ofstream plik("wyniki.txt");

powoduje otwarcie pliku tekstowego (ustawienie domyślne) do pisania. Równie dobrze można najpierw zadekla­rować zmienną typu strumie­niowego, a potem skojarzyć ją z plikiem, otwie­rając strumień za pomocą funkcji open:

ofstream plik;
plik.open("wyniki.txt");

Otwarty plik można zamknąć, używając funkcji close:

plik.close();

ale zazwyczaj nie jest to konieczne, gdyż w razie potrzeby zostanie to zrobione automa­tycznie w momencie wyjścia z bloku, w którym strumień został utworzony. Funkcje openclose są tzw. metodami – składowymi obiektu typu ofstream, podobnie jak pola są składowymi struktury. Dlatego ich wywołania, tak jak odwołania do pól struktury, mają postać notacji z kropką, tzn. są konstru­kcjami złożonymi z nazwy obiektu, kropki, nazwy metody i pary nawiasów, wewnątrz której wystę­pują argu­menty, jeśli są wymagane.

Zaprezentowany w punkcie Pliki w języku C niniejszej strony fragment programu czytania z pliku teksto­wego o nazwie Dane.txt liczby punktów i ich współ­rzędnych do dwóch tablic dynami­cznych typu double można zapisać z użyciem strumienia plikowego C++ następująco:

ifstream plik("Dane.txt");
int n;
plik >> n;
double *x = new double[n];
double *y = new double[n];
for (int i = 0; i < n; i++)
    plik >> x[i] >> y[i];
plik.close();
...
delete[] x;
delete[] y;

Wartością zmiennej plik jest referencja do strumienia typu (do obiektu klasy) ifstream utworzo­nego gdzieś w pamięci wewnę­trznej komputera w momencie otwie­rania pliku. Referencja jest wartością informu­jącą, podobnie jak wskaźnik, o położeniu tego obiektu w pamięci. Otwarcie pliku może skończyć się niepowo­dzniem, dlatego warto upewnić się, czy udało się uzyskać do niego dostęp.

Jedną z możliwości sprawdzenia stanu strumienia jest użycie go w warunku pętli lub instru­kcji warunkowej. Niezerowa wartość warunku oznacza, że poprze­dnia operacja zakończyła się pomyślnie i następna może się powieść, zaś zerowa, że poprzednia się nie udała i następna zakończy się niepowo­dzeniem. Oto przykład programu, który pobiera nazwę pliku z klawia­tury, nastę­pnie otwiera ten plik i prze­gląda go znak po znaku, zliczając liczbę wystąpień cyfr, a na koniec wypisuje tę liczbę na ekranie monitora:

#include <fstream>
#include <iostream>
#include <string>

using namespace std;

int main()
{
    string Nazwa;
    cout << "Nazwa pliku: ";
    cin >> Nazwa;
    ifstream plik(Nazwa.c_str());
    if (!plik)
    {
        cout << "Nieudane otwarcie pliku " << Nazwa << endl;
        return 1;
    }
    int n = 0;
    char c;
    while (plik >> c)
        if (c >= '0' && c <= '9')
            n++;
    cout << "Liczba cyfr w pliku: " << n << endl;
    return 0;
}

Stan strumienia sprawdzany jest tuż po próbie jego utwo­rzenia (instru­kcja if), a także po każdym wczytaniu znaku (pętla while). Niezerowa wartość wyrażenia !plik jest równo­ważna zerowej refe­rencji do pliku, czyli nieudanemu jego otwarciu, co skutkuje wyprowa­dzeniem stoso­wnego komunikatu i zakoń­czeniem programu kodem powrotu 1. Z kolei występu­jące w pętli while wyrażenie

plik >> c;

nie tylko określa operację pobierania kolejnego znaku ze stru­mienia do zmiennej c, ale też jest refe­rencją do tego stru­mienia. Dzięki temu można budować wyrażenia pobiera­jące kilka wartości na raz, jak np. współ­rzędne punktu:

plik >> x[i] >> y[i];

Gdy strumień danych zostanie wyczerpany lub na wejściu pojawi się niedozwo­lony znak, wartość takiego wyrażenia jest zerowa. Zatem występu­jące w roli warunku konty­nuacji pętli while powyż­szego programu wyrażenie powoduje przerwanie pętli czyta­jącej znak, gdy osiągnięty zostanie koniec pliku.

Strumień można wiązać z buforem znaków przechowy­wanym w pamięci wewnę­trznej. Jednym z takich strumieni jest stringstream zdefi­niowany w biblio­tece sstream, który umożliwia manipulo­wanie napisami poprzez przesy­łanie danych w obu kierun­kach (zapis i odczyt). Przykła­dowo, nazwę pliku w postaci łańcucha RRRR.txt, w którym RRRR jest zapisem wczyty­wanej z klawia­tury liczby całko­witej określa­jącej numer roku, można wygene­rować następująco:

int r;
cout << "Numer roku: ";
cin >> r;
stringstream nstr;
nstr << r << ".txt";
string nazwa = nstr.str();           // Łańcuch C++
char *nazwac = nstr.str().c_str();   // Łańcuch C

Program w C++

Zaprezentowany poniżej program tworzenia kalendarza rocznego w pliku tekstowym korzysta ze strumie­niowego wejścia-wyjścia języka C++. Kod źródłowy programu jest przenośny, nie wymaga żadnych zmian przy przecho­dzeniu od jednego do drugiego z rozpatry­wanych trzech kompila­torów. W przypadku użycia standardowej biblio­teki wejścia-wyjścia języka C było to niemożliwe, zwłaczcza przy przejściu do kompila­tora Visual C++, który traktuje niektóre funkcje tej biblio­teki za poten­cjalnie niebez­pieczne i udostępnia ich własne, specy­ficzne dla tego środo­wiska wersje.

#include <fstream>
#include <iostream>
#include <sstream>
#include <iomanip>
#include "kalend.h"

using namespace std;

string Mc[] = {"",
    "Stycze\xf1", "Luty", "Marzec", "Kwiecie\xf1", "Maj", "Czerwiec", "Lipiec",
    "Sierpie\xf1", "Wrzesie\xf1", "Pa\x9f" "dziernik", "Listopad", "Grudzie\xf1"};

void miesiac(int m, int r, ofstream &plik)
{
    plik << setw((17 - Mc[m].length()) / 2) << "" << Mc[m] << " " << r <<
            "\n --------------------\n Nd Po Wt \x8cr Cz Pt So"
            "\n --------------------\n";
    int n = dtyg(1, m, r), max = dmax(m, r);
    if (n > 0)
        plik << setw(3 * n) << "";
    for (int d = 1; d <= max; d++)
    {
        plik << setw(3) << d;
        if ((n + d) % 7 == 0)
            plik << endl;
    }
    if ((n + max) % 7)
        plik << endl;
}

int main()
{
    int r;
    cout << "Rok = ";
    cin >> r;
    stringstream nstr;
    nstr << r << ".txt";
    ofstream plik(nstr.str().c_str());
    for (int m = 1; m <= 12; m++)
    {
        miesiac(m, r, plik);
        if (m < 12)
            plik << endl;
    }
    return 0;
}

Pełne zrozumienie kodu programu może wymagać wyjaśnienia postaci trzeciego argu­mentu funkcji miesiac określa­jącego plik wynikowy – strumień typu ofstream. Poprze­dzenie nazwy argu­mentu opera­torem & oznacza, że jest ona refe­rencją – inną nazwą obiektu rzeczywi­stego skojarzo­nego z tym argu­mentem podczas wywołania funkcji. Tak więc nazwa plik używana w funkcji main określa ten sam plik, co nazwa plik używana w funkcji miesiac. Co więcej, byłoby tak nawet wtedy, gdyby te nazwy były różne, np. gdyby w funkcji miesiac zamiast plik pisać permanen­tnie kstr. Jedynym powodem użycia tej samej nazwy w obu funkcjach była chęć zacho­wania zgodności programu z jego wcześniej­szymi trzema pierwo­wzorami korzysta­jącymi z wejścia-wyjścia języka C.


Opracowanie przykładu: listopad 2018