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

Przykład C++

Dzień tygodnia Algorytmy Moduł obliczeń kalendarzowych w C++ Program w Borland C++ Program w MinGW C++ Program w Visual C++ Poprzedni przykład Następny przykład Program w Visual C# Kontakt

Dzień tygodnia

Zadaniem programu jest wczytanie trzech doda­tnich liczb całko­witych d, m i r określa­jących datę (numer dnia, miesiąca i roku) oraz wydru­kowanie w oknie konsoli, jaki dzień tygo­dnia i który z kolei dzień w roku ona przed­stawia. Kluczem do rozwią­zania jest zasada określa­nia roku przestę­pnego. Wiele osób uważa, że rok przestępny wystę­puje co 4 lata. Zazwyczaj tak jest, ale nie zawsze. Na przy­kład rok 2000 był przestępny, natomiast rok 1900 nie był, pomimo że lata 1896 i 1904 były przestę­pne. W obowią­zującym obecnie kalen­darzu grego­riańskim:

Rok jest przestępny, gdy jego numer dzieli się przez 4, ale nie dzieli się przez 100, bądź też, gdy dzieli się przez 400.

Warto przy okazji wspom­nieć, że kalen­darz grego­riański został wprowa­dzony 15 paździe­rnika 1582 roku przez papieża Grze­gorza XIII ze względu na narasta­jącą różnicę między cyklem przyro­dniczym a kalen­darzem juliań­skim, która wyno­siła wtedy 10 dni. Zniwe­lowano ją tak, że po dniu 4 paździe­rnika (czwartek) nastąpił 15 paździe­rnika (piątek). Obecnie różnica ta wynosi około dwóch tygodni. Bitwa pod Grun­waldem, jedna z najwię­kszych bitew w historii średnio­wiecznej Europy, stoczona była według ówczesnego kalen­darza 15 lipca 1410 roku, a według obecnego 9 dni później.

Polska była jednym z czterech krajów, które od razu przyjęły nowy kalen­darz (zawsze byliśmy w Europie!). Pozos­tała trójka to: Włochy, Hiszpania i Portu­galia. Później zrobiły to: Francja (20 gru­dnia 1582), Belgia i Holandia (1582–1583, 1700–1701, różne regiony zależnie od wyznania), Austria (1583), Niemcy (1583–1585, 1700), Szwaj­caria (1583–1584, 1597, 1701), Węgry (1587), Prusy (1610), Dania i Norwegia (1700), Wielka Brytania i Irlandia (1752), Szwecja i Finlandia (1753). Niektóre kraje uznały reformę kalen­darza dopiero w ubiegłym stuleciu: Bułgaria (1916), Rosja (1918), Jugo­sławia i Rumunia (1919), Grecja (1924), Turcja (1927).

Ślad późniejszego wprowadzenia kalendarza gregoriań­skiego przez Wielką Brytanię (większość obecnych tery­toriów USA była wówczas pod pano­waniem brytyj­skim) można znaleźć nawet w oprogra­mowaniu. Na przykład w kompo­nentach kalenda­rzowych Delphi i C++ Builder wrzesień 1752 roku ma tylko 19 dni.

Algorytmy

Liczby dni miesięcy kalendarza są ustalone według niewy­godnego dla obli­czeń sche­matu, a w przy­padku lutego są różne dla lat zwykłych i prze­stępnych (28 i 29), dlatego najwygo­dniej je umieścić w tablicy dwuwy­miarowej:

int n[][13] = {{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
               {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};

Jej dwa 13-elementowe wiersze są inicjali­zowane zerem i liczbami dni miesięcy dla roku zwykłego i przestę­pnego. Doda­tkowy element na początku wierszy ułatwia natu­ralną nume­rację miesięcy od 1 do 12. Gdyby wiersze były 12-elemen­towe (bez dodatkowego zera), należa­łoby miesiące nume­rować od 0 do 11. Przekształ­cenie daty reprezen­towanej przez liczby całko­wite d, mr na numer dnia w roku (liczbę dni od początku roku) sprowadza się do dodania do d sumy dni wszys­tkich miesięcy roku r poprzedza­jących miesiąc m:

int ndr(int d, int m, int r)
{
    bool p = (r % 4 == 0 && r % 100 != 0) || r % 400 == 0;
    while (--m > 0)
        d += n[p][m];
    return d;
}

Nawiasy okrągłe () w wyra­żeniu logi­cznym przypi­sywanym zmiennej p nie są konieczne (priorytety operatorów C++, p. 8, 12 i 13). Zostały wprowa­dzone, by uniknąć ostrze­żenia kompila­tora MinGW C++. Niewąt­pliwie polepszają czytel­ność kodu, nie mniej jednak kompi­lator nie powi­nien interwe­niować, gdy program jest zgodny ze specyfi­kacją języka.

Istnieją tylko dwie wartości typu bool: false – fałsz i true – prawda. W przy­padku typu int odpowia­dają im, odpowie­dnio, warto­ści 0 i 1. Być może kod stałby się bardziej przej­rzysty, gdyby użyć typu int dla zmiennej p i przypisania

int p = ((r % 4 == 0 && r % 100 != 0) || r % 400 == 0) ? 1 : 0;

w którym występuje operator warunkowy postaci:

wyrażenie_logiczne ? wyrażenie_1 : wyrażenie_2

Najpierw obliczane jest wyrażenie logiczne określające pewien warunek (tu: rok przestę­pny). Jeżeli ma ono wartość true (warunek speł­niony), wynikiem całego wyra­żenia jest wartość wyra­żenia po znaku ? (tu: 1). W prze­ciwnym razie, tj. gdy wyrażenie logiczne ma wartość false (warunek niespeł­niony), wynikiem całego wyra­żenia jest wartość wyra­żenia po znaku : (tu: 0).

Jeden z wielu algorytmów znajdowania dnia tygodnia dla podanej daty polega na obli­czeniu liczby dni, jaka dzieli tę datę od daty, dla której dzień tygo­dnia jest znany, a nastę­pnie wyko­naniu operacji modulo 7 i wybraniu tego dnia spośród siedmiu, który pasuje do otrzy­manej reszty. Przykła­dowo, od 6 stycznia 1983 roku, który to dzień przypadał w czwartek, do 25 sier­pnia 2018 roku upłynęło 13015 dni. Liczba ta po zwiększeniu o 4 (czwartek, więc przesu­nięcie o 4 dni) daje przy dzieleniu przez 7 resztę 6. Zatem dzień określony przez drugą datę przypada w sobotę.

Jest oczywiste, że taki algorytm wymaga pętli i szeregu nieco kłopo­tliwych rozstrzy­gnięć. O wiele prostsze rozwią­zanie polega na wykorzy­staniu zaczer­pniętego z amerykań­skiego czaso­pisma i nieco zmodyfi­kowanego wzoru:

w którym nawiasy kwadratowe oznaczają część całko­witą (kreska ułam­kowa odpo­wiada dzieleniu całko­witemu), mod oznacza resztę z dzielenia (modulo, opera­tor %), a liczby d, m, rw są okre­ślane następująco:

d numer dnia w miesiącu;
m przekształcony numer miesiąca: 1 – marzec, 2 – kwiecień, ..., 10 – grudzień, 11 – styczeń, 12 – luty;
r liczba reprezentowana przez dwie ostatnie cyfry pełnego numeru roku: danego – dla miesięcy od marca do grudnia, poprze­dniego – dla stycznia i lutego;
w liczba reprezentowana przez pozostałe (dwie początkowe) cyfry numeru roku.

Na przykład dla daty 25.08.2018 r. należy przyjąć: d = 25, m = 6, r = 18, w = 20, a dla daty 06.01.2000 r.: d = 6, m = 11, r = 99, w = 19. Wyzna­czona według wzoru wartość jest liczbą całko­witą od 0 do 6 reprezen­tującą dzień tygo­dnia: 0 – niedziela, 1 – ponie­działek, ..., 6 – sobota. Algorytm można sformu­łować w postaci następującej funkcji:

int dtyg(int d, int m, int r)
{
    if (m > 2)         // Jeśli miesiąc późniejszy niż luty,
        m -= 2;        // zmniejsz jego numer o 2.
    else               // W przeciwnym przypadku
    {                  // (gdy styczeń lub luty)
        m += 10;       // zwiększ numer miesiąca o 10
        r--;           // i pomniejsz numer roku o 1.
    }
    int w = r / 100;
    r %= 100;
    return (d + (13*m - 1)/5 + r + r/4 + w/4 + 5*w) % 7;
}

Funkcja otrzymuje datę przekazywaną jej w trzech argu­mentach typu int (dzień, miesiąc, rok). Warto­ścią funkcji jest liczba całkowita od 0 do 6 określa­jąca dzień tygodnia, który ta data przedstawia.

Moduł obliczeń kalendarzowych w C++

Zbiór powiązanych ze sobą funkcji i danych, na których te funkcje dzia­łają, nazy­wany jest modułem. W językach C i C++ moduł zazwy­czaj składa się z dwóch plików. Pierwszy jest plikiem nagłówkowym (ang. header file) zawiera­jącym proto­typy (nagłówki) funkcji, a ogólnie defi­nicje i dekla­racje typów, stałych, zmien­nych, funkcji itp. Plik ten ma zwykle rozszerzenie nazwy .h, wystę­puje zawsze w postaci kodu źródło­wego i stanowi tzw. interfejs służący do komuni­kowania się programu z modu­łem. Drugi plik zawiera pełny kod funkcji (defi­nicje) i innych eleme­ntów wyszczegól­nionych w pliku nagłów­kowym. Ta część modułu, zwana implemen­tacją, jest ukryta przed innymi plikami i może być dostar­czona zarówno w kodzie źródło­wym, jak i w for­macie półskom­pilowanym (ang. object file) lub skompi­lowanym (biblio­teka staty­czna lub dynami­czna).

Kod modułu może być bardzo złożony ze względu na rozbu­dowany mecha­nizm stero­wania kompila­torem, zwany prepro­cesorem, który umożli­wia makroge­nerację tekstu, kompi­lację warun­kową oraz włącza­nie do progra­mów zawar­tości wskaza­nych plików. Dyre­ktywy #include#define są prost­szymi elemen­tami języka prepro­cesora. W celu uniknię­cia błędu polega­jącego na wielo­krotnym wstawia­niu tych samych defi­nicji do programu, gdy np. kilka wchodzą­cych w jego skład modułów z nich korzysta, należy zasto­sować kompi­lację warun­kową, która w najpros­tszym przypadku ma postać:

#ifndef Nazwa_symboliczna
#define Nazwa_symboliczna
  Definicje typów, funkcji, ...
#endif

Użyta nazwa symboliczna powinna być niepowtarzalna. Prepro­cesor spraw­dza, czy nie jest ona zdefi­niowana (dyre­ktywa #ifndef). Jeżeli nie jest, defi­niuje ją (dyre­ktywa #define) i wstawia teksty defi­nicji do pro­gramu, a jeśli jest, pomija cały tekst aż do dyre­ktywy #endif. Tak więc okre­ślone defi­nicje zostaną wsta­wione do programu tylko raz – bez względu na to, ile dyre­ktyw #include poleca wsta­wienie zawiera­jącego je pliku nagłów­kowego.

W pliku nagłówkowym modułu warto na początku umieścić komen­tarz informu­jący o funkcjo­nalności modułu. W rozpa­trywanym przy­padku plik nagłów­kowy ma nazwę kalend.h, a zamie­szczony w nim komen­tarz zawiera również listę dostę­pnych funkcji i objaśnia pokrótce ich znaczenie:

// kalend.h - Funkcje dotyczące kalendarza gregoriańskiego
// ----------------------------------------------------------------------
// przest - sprawdza, czy rok r jest przestępny (false - nie, true - tak)
// dmax   - zwraca liczbę dni miesiąca m roku r
// ndr    - oblicza, który z kolei dzień w roku przedstawia data d, m, r
// dtyg   - oblicza, jaki dzień tygodnia przedstawia data d, m, r
//          (0 - niedziela, 1 - poniedziałek, ..., 6 - sobota)
// ----------------------------------------------------------------------

#ifndef H_KALEND
#define H_KALEND

bool przest(int r);
int dmax(int m, int r);
int ndr(int d, int m, int r);
int dtyg(int d, int m, int r);

#endif // H_KALEND

Kompilacja warunkowa nie jest tu konieczna, gdyż inter­fejs mudułu zawiera jedynie proto­typy funkcji, więc mogłyby się one w pro­gramie powtórzyć, jako że są tylko deklara­cjami, nie defini­cjami. Nazwy argu­mentów w proto­typach funkcji można pomi­nąć, gdyż kompi­lator je igno­ruje, ale pozosta­wienie ich dopełnia infor­macje podane w komen­tarzu.

Druga część modułu, zawarta w pliku źródłowym o nazwie kalend.cpp, zawiera defi­nicję tablicy dwuwymia­rowej, której elementy (stałe) okre­ślają liczby dni wszys­tkich mie­sięcy dla lat zwykłych i przestę­pnych, oraz kod czterech funkcji zapowie­dzianych w pliku nagłów­kowym:

#include "kalend.h"

const int n[][13] = {{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
                     {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};

bool przest(int r)
{
    return (r % 4 == 0 && r % 100 != 0) || r % 400 == 0;
}

int dmax(int m, int r)
{
    return n[przest(r)][m];
}

int ndr(int d, int m, int r)
{
    bool p = przest(r);
    while (--m > 0)
        d += n[p][m];
    return d;
}

int dtyg(int d, int m, int r)
{
    if (m > 2)
        m -= 2;
    else
    {
        m += 10;
        r--;
    }
    int w = r / 100;
    r %= 100;
    return (d + (13 * m - 1) / 5 + r + r / 4 + w / 4 + 5 * w) % 7;
}

Dyrektywa #include jest tu niezbędna ze względu na wyma­gania kompila­tora Visual C++. Gdyby jej jednak nie było, istotna byłaby kolej­ność poszcze­gólnych defi­nicji. I tak np. funkcja przest nie mogłaby wystą­pić po dmax, a tablica n po ndr. W języku C++ nie można bowiem użyć nazwy, która nie została uprze­dnio zadekla­rowana. Wyma­ganie Visual C++ wydaje się więc – przynaj­mniej po części – usprawie­dliwione.

Nazwa pliku nagłówkowego została ujęta w dyre­ktywie #include nie w nawiasy kątowe, lecz w cudzy­słowy. Oznacza to, że plik ten nie jest umie­szczony w standar­dowym folderze plików włącza­nych, lecz gdzie indziej. Ściślej mówiąc, plik ten znajduje się w fol­derze bieżącym, gdyż jego nazwa nie jest poprze­dzona ścieżką dostępu.

Program w Borland C++

Program ma wczytać trzy liczby całko­wite określa­jące datę kalen­darza gregoriań­skiego oraz wyznaczyć odpowia­dajacy jej dzień tygo­dnia i numer dnia od początku roku. Po utwo­rzeniu projektu programu dodajemy do niego dwa nowe pliki o rozsze­rzeniach nazwy .h.cpp, wybie­rając pole­cenia Project New Source File  H (rys.) i Project New Source File  C++. Środo­wisko nadaje im wstę­pnie nazwy untitled1.huntitled1.cpp. Pliki mają stanowić omó­wiony wyżej moduł obli­czeń kalenda­rzowych, toteż po ich edycji zapi­sujemy je jako kalend.hkalend.cpp (pole­cenia File Save As...).

Inne rozwiązanie włączenia modułu do pro­gramu polega na skopio­waniu przygo­towanych wcześniej plików kalend.hkalend.cpp do folderu programu i dodaniu ich do projektu za pomocą pole­cenia Project Add File. Moduł może znaj­dować się również w innym folderze. Plik główny programu zawie­rający funkcję main ma postać:

#include <iostream>
#include <conio.h>
#include "kalend.h"

using namespace std;

const string Nazwa[] = {"Niedziela", "Poniedzialek", "Wtorek", "Sroda",
                        "Czwartek", "Piatek", "Sobota"};
int main()
{
    int d, m, r;
    cout << "Dzien  : ";
    cin >> d;
    cout << "Miesiac: ";
    cin >> m;
    cout << "Rok    : ";
    cin >> r;
    cout << Nazwa[dtyg(d, m, r)] << ", " << ndr(d, m, r) << " dzien roku.\n";
    _getch();
    return 0;
}

Elementami tablicy Nazwa są łańcuchy C++ określa­jące nazwy siedmiu dni tygo­dnia. Po wprowa­dzeniu daty wypisy­wana jest jedna z tych nazw – ta o indeksie obli­czonym przez funkcję dtyg, a po niej przecinek, liczba dni wyzna­czona przez funkcję ndr i tekst wyjaśnia­jący jej znaczenie. Gdyby moduł obli­czeń kalenda­rzowych znajdował się poza folderem programu, nazwa jego pliku nagłów­kowego w dyre­ktywie #include powinna być poprze­dzona stosowną ścieżką dostępu.

Program w MinGW C++

W środowisku Code::Blocks pole­cenie File New File... otwiera okno New from template, w którym ukazane są m.in. dwie ikony odpowie­dzialne za tworzenie obydwu plików nowego modułu. Ikona C/C++ header wiedzie do okna krea­tora pliku nagłów­kowego modułu (rys.), w którym należy podać nazwę pliku wraz z pełną ścieżką dostępu (Filename with full path), nazwę symbo­liczną używaną do stero­wania kompi­lacją warun­kową modułu (Header guard word) oraz opcje włą­czenia pliku do proje­ktu i wyboru trybu kompilacji (DebugRelease). Analogicznie, ikona C/C++ source prowa­dzi do okna krea­tora pliku źródło­wego stano­wiącego część implemen­tacyjną modułu (tym razem w oknie nie ma pola Header guard word). Należy pamiętać, by w opcjach włączyć oba pliki do proje­ktu i wybrać przy­najmniej jeden tryb kompi­lacji, gdyż w przeci­wnym razie program nie daje się skompi­lować.

Podobnie jak w przypadku środowiska Relo, przygo­towane wcze­śniej pliki kalend.hkalend.cpp można skopiować do folderu programu, a nastę­pnie dodać je do projektu za pomocą pole­cenia Project Add Files... . Oczy­wiście mogą one znaj­dować się w innym folderze, a wówczas w dyre­ktywie #include programu należy poprze­dzić nazwę pliku nagłów­kowego ścieżką dostępu. Gdy folder programu zawiera pliki modułu, plik programu ma postać:

#include <iostream>
#include "kalend.h"

using namespace std;

const string Nazwa[] = {"Niedziela", "Poniedzialek", "Wtorek", "Sroda",
                        "Czwartek", "Piatek", "Sobota"};
int main()
{
    int d, m, r;
    cout << "Dzien  : ";
    cin >> d;
    cout << "Miesiac: ";
    cin >> m;
    cout << "Rok    : ";
    cin >> r;
    cout << Nazwa[dtyg(d, m, r)] << ", " << ndr(d, m, r) << " dzien roku.\n";
    return 0;
}

Powyższy program różni się od programu dla kompilatora Borland C++ jedynie tym, że nie używa biblio­teki conio i funkcji _getch. Wynika to stąd, że środo­wisko Code::Block w przeci­wieństwie do Relo wstrzy­muje zamknię­cie okna konsoli po wyko­naniu programu.

Program w Visual C++

Zakładamy, że projekt programu nie zawiera prekompi­lowanego nagłówka, a jedynie plik źródłowy Dzien.cpp z funkcją main – powiedzmy, że dokła­dnie taki sam jak dla MinGW C++. Braku­jącym eleme­ntem programu jest oczy­wiście moduł złożony z pliku nagłów­kowego kalend.h i pliku implemen­tacyjnego kalend.cpp. W celu utwo­rzenia pierw­szego pliku i dodania go do proje­ktu wybie­ramy pole­cenie Projekt Dodaj nowy element..., a nastę­pnie po ukazaniu się okna Dodaj nowy element – Dzien (rys.) zazna­czamy opcję Plik nagłówka (.h), w polu Nazwa wpisu­jemy kalend.h i naci­skamy przycisk Dodaj. Podobne opera­cje wykonu­jemy dla drugiego pliku. Oba pliki zostały utwo­rzone w fol­derze programu, ale ich domyślną lokali­zację można było zmienić.

Dyrektywa #pragma once w utworzonym przez środo­wisko pliku nagłów­kowym określa, że plik ten zostanie włączony do programu tylko raz. Jednak nie jest ona zgodna ze specy­fikacją języka C++, dlatego lepiej zabez­pieczać się przed wielo­krotnym wsta­wianiem pliku do programu, uży­wając dyrektyw warun­kowych #ifndef—#endif.

Przygotowane wcześniej oba pliki modułu obliczeń kalenda­rzowych umieszczone w dowolnym folderze można włączyć do proje­ktu programu za pomocą pole­cenia Projekt Dodaj istniejący element... . Gdy znajdują się w folde­rze programu, jego kod źródłowy może mieć nastę­pującą postać:

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

using namespace std;

const string Nazwa[] = {"Niedziela", "Poniedzialek", "Wtorek", "Sroda",
                        "Czwartek", "Piatek", "Sobota"};
int main()
{
    int d, m, r;
    cout << "Dzien  : ";
    cin >> d;
    cout << "Miesiac: ";
    cin >> m;
    cout << "Rok    : ";
    cin >> r;
    cout << Nazwa[dtyg(d, m, r)] << ", " << ndr(d, m, r) << " dzien roku.\n";
    return 0;
}

W porównaniu z wersją dla kompilatora MinGW C++ w kodzie programu pojawiła się dyre­ktywa #include włącza­jąca plik nagłów­kowy biblio­teki string. Podo­bnie jak w pro­gramie zapisu liczby w notacji rzym­skiej, wyprowa­dzenie łańcu­cha C++ do stru­mienia cout wymaga użycia biblio­teki string. A oto przykła­dowe wyniki wyko­nania programu dla daty Wiktorii Wie­deńskiej:


Opracowanie przykładu: sierpień 2018