E-book
27.03
drukowana A5
49.97
drukowana A5
Kolorowa
80.59
C++20. Laboratorium

Bezpłatny fragment - C++20. Laboratorium


Objętość:
371 str.
ISBN:
978-83-8245-716-2
E-book
za 27.03
drukowana A5
za 49.97
drukowana A5
Kolorowa
za 80.59

Słowem wstępu

Na wstępie chciałbym zdementować plotki, że pracuję dla Umbrella Corporation nad cyfrową bronią w postaci wirusa komputerowego. Owszem, korporacja Umbrella pokryła raz czy dwa koszty serwera na którym trzymam swojego bloga, ale nic więcej. Drogi Czytelniku, w tym dokumencie przekazuję Ci szkolenie z języka C++ z elementami wersji C++20.


Poznaj C++ — język hakerów!


Wszelkie znaki towarowe i nazwy zastrzeżone zostały użyte jedynie w celach informacyjnych i należą wyłącznie do ich prawnych właścicieli. Autor w czasie tworzenia tej treści nie jest zawodowo powiązany z firmami, których technologie i produkty opisuje. Autor w tej publikacji nie działa też w imieniu tych firm.

Postać Mr. At nawiązuje do gier z gatunku roguelike/ASCII RPG w których do wyświetlania grafiki korzysta się ze znaków tekstowych. Znak @ (At) w grach roguelike przeważnie oznacza postać bohatera.

Rozdział 0x01. Wprowadzenie

1.1. Schemat blokowy i pseudokod

Programowanie to w dużym uproszczeniu wydawanie poleceń komputerowi. Zestaw takich poleceń to algorytm. Można to porównać do przepisu czy receptury na osiągnięcie określonego wyniku. Algorytm może być opisany słownie, w punktach jako lista kroków, jako schemat blokowy, czy też pseudokod albo kod programu w określonym języku.

1.1.1. Infekcja plików przez wirusy komputerowe

Tradycyjne, typowe wirusy komputerowe działają na zasadzie powielania samych siebie, często poprzez doklejanie się do istniejących plików.


Opis słowny algorytmu infekcji

Program wyszukuje na dysku komputera pliki oraz sprawdza czy jest do nich doklejony. Jeśli plik nie jest zainfekowany, to dokleja swój kod do znalezionego pliku.


Lista kroków algorytmu infekcji

1. Wyszukaj plik na dysku komputera

2. Sprawdź czy zawiera doklejony kod wirusa

3. Jeśli plik jest zainfekowany wróć do punktu 1

4. W przeciwnym wypadku doklej się do znalezionego pliku

5. Wróć do punktu 1

Schemat blokowy algorytmu infekcji

Algorytm infekcji w formie schematu blokowego przedstawia rysunek 1.1.

Rysunek 1.1. Schemat blokowy prostego algorytmu infekcji wirusem komputerowym

Schematy blokowe składają się z elementów, które później łatwo odwzorować w kodzie programu. Kształt elipsy to początek i koniec. Równoległobok to pobieranie i wyświetlanie danych. Romb to sprawdzenie warunku (tak/nie). Natomiast prostokąt to blok wykonywania określonej operacji. Poszczególne elementy są łączone strzałkami.

Nieco bardziej zaawansowanym sposobem przedstawienia algorytmu jest napisanie go jako pseudokod.


Jest tutaj duża dowolność. Może występować mieszanie słów z języka polskiego czy angielskiego z poleceniami komputerowymi.


Przykładowy pseudokod algorytmu infekcji


start:

plik = odczytaj_plik_z_dysku_komputera();

warunek = czy_plik_zainfekowany(plik);

if (warunek == prawda) goto start;

if (warunek == fałsz) infekuj(plik);

goto start;


Pseudokod nie jest kodem źródłowym, który można bez błędów kompilatora zbudować. Ma jedynie przedstawić schemat działania algorytmu czy sposób myślenia.

1.2. Kod źródłowy programu i kompilacja

Przeważnie chcąc napisać program komputerowy tworzymy plik tekstowy w składni odpowiedniej dla danego języka programowania. Kod z poleceniami i innymi konstrukcjami składniowymi nazywany jest kodem źródłowym.

Może on zawierać różne elementy zależnie od składni, ale są części wspólne dla wielu języków programowania.


Jednym z podstawowych elementów są zmienne. Można sobie je wyobrazić jako nazwane komórki w pamięci, w których przechowuje się różne dane np. wartości liczbowe, napisy, ciągi elementów (tablice) itd. Jak sama nazwa wskazuje — zmienna, czyli „zmienia się” — zatem możliwe jest modyfikowanie tego miejsca w pamięci. Tak oto w programie zależnie od potrzeb można dodawać wartości liczbowe, odejmować je, sklejać napisy, podmieniać poszczególne znaki w napisach, odczytać plik rysunku z dysku do pamięci i zmieniać w nim kolory itd.


Kolejnym ważnym elementem w większości programów jest procedura, nazywana też podprogramem. Wyobraźmy sobie, że napisaliśmy kod, który rysuje na ekranie okno z komunikatem informacyjnym. Dzięki procedurze nie wklejamy tego samego kodu w każde miejsce, w którym ma być wyświetlony komunikat. Definiujemy procedurę z kodem, który rysuje okno informacyjne, a wywołujemy go gdzie tylko chcemy poprzez podanie nazwy tej procedury.


Podobnym mechanizmem do procedury jest funkcja. Nazwy te są czasem stosowane zamiennie, ale raczej przyjmuje się, że funkcja powinna zwracać wartość. Na przykład: Funkcja rysująca okno komunikatu może zwrócić wartość określającą czy użytkownik programu zamknął to okno, czy kliknął OK lub inny przycisk.

Na rysunku 1.2 przedstawiono schemat ogólny procesu kompilacji (budowania) programu z kodu źródłowego do kodu binarnego (aplikacji) na przykładzie Visual C++.

Rysunek 1.2. Proces budowania programu w Visual C++ (od kodu źródłowego w C++ przez Asembler, aż do postaci binarnej)

Rozdział 0x02. C++, czyli poznaj język hakerów

https://github.com/hakerinfo/c-plus-plus-basics

W tym rozdziale znajduje się kurs języka C++, a dokładnie odmiany Visual C++ dla systemów z rodziny Windows. Środowisko programistyczne wymagane do budowania prezentowanych tutaj kodów źródłowych to Microsoft Visual Studio. Narzędzie to występuje też jako wersja darmowa, dostępna do pobrania na witrynie Microsoft.


https://visualstudio.microsoft.com/


Przykłady zaprezentowane dalej można pobrać na swój komputer z konta GitHub autora tej książki, a dokładnie z repozytorium:

https://github.com/hakerinfo/c-plus-plus-basics

2.1. NOT-A-VIRUS-Viral-Hello-MSVC++

Wprowadzenie do programowania w określonym języku często rozpoczyna się od stworzenia aplikacji typu „Witaj, świecie!” (ang. hello world). Tego rodzaju program wyświetla napis o wspomnianej treści, a jego opis pozwala zrozumieć podstawowe elementy składni i główne zasady panujące w poznawanym języku.


Stworzenie programu wyświetlającego napis (tekst) jest jednocześnie ćwiczeniem i rozpoczęciem nauki w sposób praktyczny.


Kod źródłowy aplikacji typu Hello, world! przedstawia rysunek 2.1. Jeśli jesteś początkujący, to proszę się przyjrzeć strukturze kodu i niczym nie martwić. W tym podrozdziale będzie opis oraz analiza prezentowanego przykładu, a w kolejnych instrukcje jak budować plik wykonywalny (*.exe) oraz go uruchamiać w środowisku Microsoft Visual Studio.

Rysunek 2.1. Wyświetlenie napisu na standardowym wyjściu w języku C++

Czas na przeanalizowanie kodu źródłowego z rysunku 2.1. Po lewej stronie rysunku są numery linii, których oczywiście nie piszemy, po prostu wyświetla je edytor, aby łatwo nawigować po źródle.

Pierwsza linia zawiera dyrektywę preprocesora, której podano plik nagłówkowy o nazwie iostream, aby został dołączony przy tworzeniu programu. Angielskie słowo process oznacza przetwarzać. Natomiast przedrostek pre można rozumieć jako przed. Zatem dyrektywy („nakazane czynności”) preprocesora oznaczają, że coś musi być przetworzone, zanim się przejdzie do kodu źródłowego programu. W prostych słowach: dyrektywą #include dołączamy pliki nagłówkowe z „rzeczami”, których potrzebujemy użyć w pisanym dalej kodzie programu. W przykładzie na rysunku 2.1 dołączony jest nagłówek iostream, czyli obsługa strumienia wejścia-wyjścia (ang. input-output stream). To w nim jest zawarty (zaimplementowany) używany w kodzie obiekt (cout), który u nas wyświetla tekst na konsoli (ekranie). Dalej w treści będą dołączane inne pliki nagłówkowe, zależnie od potrzeb np. fstream do obsługi strumieni plików czy Windows. h do korzystania z dobrodziejstw udostępnianych przez system Windows.


Przejdźmy do linii trzeciej (3) w kodzie na rysunku 2.1. W tej linii znajduje się definicja funkcji głównej programu (main). Natomiast nawiasy klamrowe w liniach czwartej (4) oraz jedenastej (11) określają ciało funkcji (ang. body), czyli instrukcje zawarte w jej wnętrzu. W języku C++ klamry są też używane do wydzielania bloków (fragmentów) kodu np. w celu, aby określone elementy były widoczne/dostępne tylko wewnątrz takiego bloku. Funkcja główna main jest punktem wejścia (ang. entry point) tworzonej aplikacji. W aplikacjach działających pod kontrolą systemu operacyjnego, gdy uruchamiamy plik. exe aplikacji, to system Windows rozpoczyna wykonywanie programu właśnie od początku funkcji main. Pozostając jeszcze przy linii trzeciej (3) — rysunek 2.1 — znajduje się w niej nagłówek funkcji, czyli typ zwracanej wartości (tutaj int), nazwa funkcji (tutaj main) oraz argumenty (w nawiasach okrągłych, tutaj brak argumentów). Typ zwracanej wartości przed nazwą funkcji określa jaki rodzaj (typ) danych ona zwraca. Funkcja main powinna zwracać wartość liczbową typu int (liczba całkowita). Inne funkcje mogą zwracać różne wartości np. napis, obiekt czy ciąg elementów (np. tablicę).


Dla przykładu z rysunku 2.1 pozostało jeszcze przeanalizować wnętrze funkcji (kod pomiędzy nawiasami klamrowymi). Jest tutaj dość sporo elementów, jak na „zwykłe” wyświetlenie tekstu na ekranie. Z tego powodu analizę (oprócz opisu) przedstawiono również na rysunku 2.2. Wnętrze funkcji z rysunku 2.1 zawiera trzykrotne przekazanie danych do strumienia i wyświetlenie ich na standardowym wyjściu (obiekt cout — wymowa „see out”). Można to było zapisać w pojedynczej instrukcji (jedno użycie cout), ale w celu pokazania kilku wywołań użyto takiego schematu. Obiekt cout (skrót od character out) zajmuje się obsługą standardowego strumienia wyjścia, czyli w aplikacji konsolowej dla Windows po prostu wyświetla dane na ekranie konsoli tekstowej.

Możliwość użycia cout daje nam dołączony wcześniej nagłówek iostream. Obiekt cout znajduje się w przestrzeni nazw std. Może nastąpić sytuacja, że będzie potrzeba stworzyć własny obiekt i nazwać go tak samo (cout), zatem w celu uniknięcia konfliktu nazewnictwa, definiowane elementy umieszcza się w przestrzeniach nazw. Dostęp do obiektu cout w przestrzeni nazw std uzyskiwany jest poprzez operator rozpoznawania zakresów (dwa znaki dwukropka), czyli std::cout.


Dalej za pomocą operatora wstawiania do strumienia (<<) przesyłamy napis (tekst), który w języku C++ umieszcza się w cudzysłowach. Pojedyncza instrukcja kończona jest znakiem średnika. W liniach siódmej (7) i dziewiątej (9) rysunku 2.1 można zauważyć operator wstawiania (<<), jednak nie ma bezpośrednio przed nim obiektu cout. Warto zwrócić uwagę na to, że napis w tych liniach jest podzielony (w dwóch częściach), zatem zastosowanie operatora wstawiania (<<) powoduje przesyłanie tych dwóch części napisu na standardowe wyjście (cout). Poszczególne instrukcje w języku C++ nie muszą się kończyć wraz z przełamaniem linii. Koniec instrukcji określa znak średnika. Zatem blok ciała funkcji z rysunku 2.1 ma w sumie cztery instrukcje (zakończone średnikiem). Trzy użycia obiektu cout oraz instrukcja powrotu z funkcji (return).

Rysunek 2.2. Program typu Hello, world! w C++ z graficznymi objaśnieniami

2.2. Budowanie i uruchamianie projektu

W poprzednim podrozdziale przedstawiono kod źródłowy programu typu „Hello, world!” w C++ z teoretycznym opisem. Czas poznać jak tworzyć projekt aplikacji w środowisku Microsoft Visual Studio oraz jak zbudować kod źródłowy programu w postaci tekstu do pliku wykonywalnego (.exe), który można uruchomić. Dalszy opis pomija instalację środowiska programistycznego. W celu utworzenia projektu aplikacji należy uruchomić Visual Studio i kliknąć Utwórz nowy projekt (rysunek 2.3). Interfejs graficzny kolejnego okna daje możliwość wyboru wielu języków programowania, platform czy typów projektów. Uniwersalny sposób to wybranie języka C++, platformy Windows i odnalezienie szablonu Kreator aplikacji klasycznej systemu Windows (rysunek 2.4). Kolejny krok pozwala ustawić nazwę projektu, miejsce na dysku na pliki oraz nazwę rozwiązania (rysunek 2.5). Rozwiązanie (ang. solution) ma za zadanie „grupować” projekty, czyli jedno rozwiązanie może zawierać wiele projektów.


Dalej pojawia się możliwość wybrania rodzaju projektu (rysunek 2.6):

— Aplikacja konsolowa (exe) — program działający w trybie tekstowym,

— Aplikacja klasyczna (exe) — program przeważnie okienkowy, ale niekoniecznie (nie musi tworzyć okna),

— Biblioteka dołączana dynamicznie (dll) — komponent udostępniający określoną funkcjonalność, który jest osobnym plikiem z rozszerzeniem. dll i mogą go ładować i używać inne programy,

— Biblioteka statyczna (lib) — komponent udostępniający określoną funkcjonalność, który łatwo może zostać dołączony do programu poprzez „wbudowanie” go w plik aplikacji.

Dla bieżącego przykładu należy wybrać opcję Aplikacja konsolowa (rysunek 2.6). W tym samym oknie możliwe jest ustawienie opcji dodatkowych. Tutaj (rysunek 2.7) należy wybrać Pusty projekt.

Rysunek 2.3. Tworzenie nowego projektu w Microsoft Visual Studio


Rysunek 2.4. Wybór szablonu projektu w środowisku Visual Studio


Rysunek 2.5. Ustalenie nazwy projektu, nazwy rozwiązania i miejsca na pliki projektu w środowisku Visual Studio


Rysunek 2.6. Wybranie typu tworzonej aplikacji w środowisku Visual Studio


Rysunek 2.7. Wybranie opcji dodatkowych dla projektu w Visual Studio (tutaj zaznaczenie Pusty projekt)

Na tym etapie (rysunki od 2.3 do 2.7) utworzony jest pusty projekt dla języka C++ w środowisku Microsoft Visual Studio. Kod źródłowy w projekcie jest zapisywany w plikach z rozszerzeniem. cpp (pliki źródłowe) oraz. hpp (pliki nagłówkowe). Dla bieżącego przykładu należy dodać plik źródłowy o nazwie main. cpp w oknie dokowanym Eksploratora rozwiązań, tak jak na rysunkach 2.8 oraz 2.9.

Rysunek 2.8. Dodawanie nowego elementu (tutaj pliku źródłowego w C++) do projektu w Visual Studio (krok 1)


Rysunek 2.9. Dodawanie nowego elementu (tutaj pliku źródłowego w C++) do projektu w Visual Studio (krok 2)

Po wykonaniu kroków z rysunków 2.8 oraz 2.9 w oknie dokowanym Eksploratora rozwiązań w gałęzi Pliki źródłowe powinien pojawić się element (plik) main. cpp. W celu wpisania kodu źródłowego do tego pliku, należy podwójnie kliknąć jego nazwę (w środowisku Visual Studio) oraz wprowadzić przykładowy kod jak na rysunku 2.10.

Rysunek 2.10. Przykładowy program typu „Hello, world!” w C++ (edycja pliku main. cpp)

Warto wspomnieć tutaj o opcji przedstawionej na rysunku 2.11. Pozwala ona wybrać architekturę docelową. Możemy zdecydować czy zbudowany program ma być dla Windows 32-bit (x86) czy dla Windows 64-bit (x64).

Rysunek 2.11. Wybranie architektury pozwala określić czy chcemy zbudować program dla Windows 32-bit (x86) czy Windows 64-bit (x64)

Gdy plik main. cpp zawiera już przykładowy kod, można zbudować projekt. Proces ten nazywany jest kompilacją. W tym celu należy kliknąć Kompiluj rozwiązanie (rysunek 2.12).

Rysunek 2.12. Budowanie projektu w Visual Studio (kompilacja)

Gdy proces kompilacji przebiegnie pomyślnie (rysunek 2.12), to można uruchomić program „pod kontrolą” (rysunek 2.13) środowiska Visual Studio. Debugowanie czy też odpluskwianie to uruchomienie aplikacji pod nadzorem narzędzia nazywanego debuggerem (bug — robak — tutaj błąd). Wtedy podczas wystąpienia błędu w programie możliwe jest uzyskanie większej ilości informacji np. co spowodowało błąd, w którym miejscu, jakie są wartości zmiennych w danym momencie itd.

Rysunek 2.13. Uruchomienie programu pod kontrolą debuggera w Visual Studio

Etap przedstawiony na rysunku 2.12 (kompilacja) utworzył pliki wykonywalne programu. Ten plik (.exe) możemy dystrybuować i nie jest potrzebne Visual Studio, aby ktoś uruchomił nasz program. Jest to gotowy plik wykonywalny.


Zbudowany program można znaleźć w folderze przedstawionym na rysunku 2.14. Jednak do dystrybucji czy publikowania swoich programów należy zmienić dość ważną opcję. Budowanie może być w trybie Debug oraz Release. Druga wersja (Release) nadaje się do wydania programu, plik wtedy jest m.in. bardziej zoptymalizowany. Wtedy też plik wykonywalny po kompilacji będzie umieszczony w folderze Release, nie Debug (rysunek 2.14).

Rysunek 2.14. Miejsce utworzenia pliku wykonywalnego pisanego programu w Visual Studio

W przypadku aplikacji konsolowej z kodem jak w bieżącym przykładzie, niektórych może zaskoczyć, że po uruchomieniu pliku Project1.exe (rysunek 2.14) na ekranie pojawi się tylko mrugnięcie konsoli. Aplikacje konsolowe mają przeważnie taką cechę, że wykonują określone działanie i zaraz się zamykają. Program z bieżącego przykładu wyświetla tekst na konsoli i również się zamyka, a dzieje się to dość szybko.

Dobrą metodą, aby zobaczyć wyjście (wynik) tego typu aplikacji konsolowej, jest uruchomienie jej w oknie Wiersza polecenia (rysunek 2.15).

Rysunek 2.15. Odnalezienie i uruchomienie Wiersza polecenia w systemie Windows

W celu uruchomienia programu konsolowego bez wpisywania poleceń do Wiersza polecenia (cmd. exe) można po prostu przeciągnąć i upuścić (kursorem myszy) plik wykonywalny programu w C++ (Project1.exe) na okno Wiersza polecenia (rysunek 2.16).

Rysunek 2.16. Przeciągnięcie i upuszczenie pliku programu (.exe) na okno Wiersza polecenia w celu uruchomienia

Napis wyświetlany przez program stworzony w C++ przedstawiono na rysunku 2.17. Jednak pojawił się tutaj mały kłopot, gdyż okno Wiersza polecenia, niepoprawnie pokazuje polskie znaki (ąśęó…). Można to naprawić dopisując do kodu programu w C++ wywołanie funkcji setlocale();, tak jak na rysunku 2.17. Należy pamiętać, że zmieniając kod źródłowy np. poprzez dopisanie czegoś, wymagana jest ponowna kompilacja, aby zmiany były uwzględnione w wynikowym pliku wykonywalnym (.exe).

Rysunek 2.17. Uruchomienie programu w C++ w Wierszu polecenia i naprawienie wyświetlania polskich znaków

2.3. Komentarze w języku C++

W zawodowym programowaniu przeważnie wyznaje się zasadę, że kod powinien być samo-komentujący się. Oznacza to, że komentarze (opisy) ograniczone są do minimum. Zapis kodu źródłowego ma być tak stworzony, a zmienne i inne elementy tak nazwane, aby kod był zrozumiały bez opisów w komentarzach.

Jednak podczas poznawania nowego języka programowania czy ogólnie nauki programowania, nic nie stoi na przeszkodzie, aby stosować jak najwięcej komentarzy. Ten kod jest dla Ciebie, uczysz się, więc możesz sobie opisywać nawet najprostsze konstrukcje.

Jak poznawałem pierwsze języki programowania, to pamiętam, że w kodach, które pisałem było bardzo dużo komentarzy. Nawet zdarzały się opisy w stylu:

//to jest zmienna z początkową wartością zero

//tu ma być średnik

//tu nie może być średnika

//to jest tablica — elementy numerujemy od zera (!)

i tym podobne.

Przejdźmy jednak do komentarzy w języku C++. Opisy te są pomijane przez kompilator przy budowaniu programu. Mają jedynie cel informacyjny dla osoby czytającej kod źródłowy.

Komentarz w C++ może być jednoliniowy. Zaczyna się dwoma znakami //, czyli slash (ukośnik), a kończy wraz z końcem bieżącej linii.

Istnieje również komentarz wieloliniowy, w którym możemy wstawić nawet akapit tekstu. Rozpoczyna się znakami /*, a kończy znakami */.

Przykładowy kod z powyższymi rodzajami komentarzy przedstawiono na rysunku 2.18.

Rysunek 2.18. Przykład stosowania komentarzy jednoliniowych i wieloliniowych do kodu w języku C++

2.4. Typy danych, zmienne i stałe

Język C++ posiada różne typy (rodzaje) danych. Na przykład liczba całkowita jest czymś innym (jest inaczej traktowana), niż litera z alfabetu. W poprzednich rozdziałach było wspomniane, że napis definiuje się wewnątrz cudzysłowów np. „Hello” to jest napis (ciąg znaków). Dalej, liczba całkowita (int od słowa integer) to np. 128. Jako, że jest to typ liczbowy możemy w programie wykonać dodawanie pisząc: 128 +128. Czym zatem jest zapis „128”? To napis, ciąg znaków.

Przyjrzyjmy się jeszcze zmiennym. Można je wyobrazić sobie jako pojemniki na dane komputerowe czy komórki w pamięci, które przechowują określone wartości. W pamięci komputera z punktu widzenia wykonującego się programu wszystko jest wartościami liczbowymi (bajtami) — tylko różnie interpretowanymi (rozpoznawanymi). Na tym etapie nie określajmy jednak zmiennych jako „pojemnik na wszystko”. Zmienne w C++ mają określony typ (rodzaj) danych, które mogą przechowywać. Aby korzystać ze zmiennej należy ją stworzyć za pomocą odpowiedniego zapisu w języku C++.

2.4.1. Zmienne

Po wstępie czym są zmienne i stwierdzeniu, że mogą być one różnego rodzaju (typu) czas przejść do strony praktycznej.

Definicja zmiennej w C++ tworzona jest według schematu:

typ nazwa;


Zatem w celu utworzenia zmiennej przechowującej napis można użyć zapisu:

std::string zmienna = „tekst”;


Gdzie std to przestrzeń nazw. Można sobie wyobrazić to jako przestrzeń w której „zamknięte” są różne elementy. Dostęp do danej przestrzeni da się uzyskać poprzez operator rozpoznawania zakresów (ang. scope), który jest zapisywany jako dwa dwukropki. Słowo zmienna to identyfikator (nazwa). Dalej jest operator przypisania (znak równości) oraz wartość, tutaj napis.


Fragment kodu, który tworzy zmienną o określonym typie i nazwie to definicja. Inne słowo, którego znaczenie jest równie ważne to deklaracja. Pojęcia te są często mylone. W prostych słowach deklaracja określa jak napisane przez nas nazwy (nie tylko zmiennych) mają być interpretowane.


Definicja jest jednocześnie deklaracją, ale deklaracja nie jest definicją. Powołanie do istnienia nazwy (tylko nazwy) z typem i ewentualnymi atrybutami w kodzie jest wyłącznie deklaracją.


Fragment kodu definiujący zmienną typu liczbowego int:

int a = 7;

Jest definicją (kompilator zarezerwuje pamięć dla tej zmiennej w programie i wygeneruje kod maszynowy) i jednocześnie deklaracją (zawiera informacje, że taka nazwa istnieje).


Trzecim pojęciem na które należy również zwrócić uwagę jest inicjalizacja. Sama definicja zmiennej int a; powoduje w języku C++, że pod tym miejscem w pamięci (w tej zmiennej) będzie nieokreślona wartość nazywana przez niektórych śmieci. Chodzi tutaj o zwrócenie uwagi na to, aby nie zakładać, że dana zmienna liczbowa ma wartość zero czy, że jakiś bufor pamięci będzie wyzerowany, albo miał określoną wartość. Nie można polegać na takich założeniach. Środowisko Microsoft Visual Studio ostrzega przed użyciem zmiennych bez nadanej wartości początkowej i blokuje budowanie programu.


Spróbujmy zatem jako laboratorium podejrzeć co jest w zmiennych, którym nie nadamy wartości. Kod przykładowego programu i miejsce kliknięcia w celu ustawienia pułapki (ang. breakpoint) prezentuje rysunek 2.19. Następnie należy uruchomić program w trybie debugowania w środowisku Visual Studio (przycisk Rozpocznij debugowanie). Wykonywanie kodu zatrzyma się na ustawionej pułapce (rysunek 2.20). Wtedy poprzez najechanie kursorem myszy na nazwę zmiennej, możliwe jest wyświetlenie jej wartości (rysunek 2.20). Jak widać, zmienna nie jest wyzerowana. Dlatego warto w języku C++ zawsze nadawać zmiennym wartości początkowe (inicjalizacja).

Rysunek 2.19. Ustawianie pułapki (ang. breakpoint) w środowisku Microsoft Visual Studio dla programu w C++


Rysunek 2.20. Podejrzenie wartości niezainicjalizowanej zmiennej typu int podczas debugowania programu w środowisku Microsoft Visual Studio

Rysunek 2.21.1 przedstawia przykładowe definiowanie zmiennych liczbowych typu int oraz napisów typu string. Do wstawienia znaku nowej linii zostało tutaj użyte std::endl zamiast "\r\n” jak w poprzednich przykładach. W bieżącym przykładzie nic widocznego się nie zmienia, ale warto znać też ten sposób.

W kodzie z rysunku 2.21.1 zaprezentowano również użycie operatora dodawania (znak plus). Umieszczając ten operator pomiędzy liczbami typu int wykonało się dodawanie. Natomiast umieszczenie pomiędzy napisami, połączyło (skleiło) te napisy. Próba połączenia operatorem dodawania napisu string oraz liczby int spowoduje błąd i kompilator nie zbuduje programu. Zmierzam do tego, że operatory mogą być przeciążane. Oznacza to, że znak dodawania w przypadku np. liczb typu int je zsumuje, a w przypadku napisów std::string je sklei (połączy). Nic nie stoi na przeszkodzie programując np. grę typu RPG, aby przeciążyć operator dodawania tak, że jak poda mu się obiekty eliksir zielony oraz liść elfickiej rośliny, to utworzy w ekwipunku gracza obiekt mniejszy napój energii magicznej. Choć to oderwany od rzeczywistości przykład, to myślę, że wiadomo na czym ogólnie polega przeciążanie operatorów. Więcej o operatorach, ich priorytetach oraz przeciążaniu jest w dalszych rozdziałach.

Rysunek 2.21.1. Definiowanie zmiennych liczbowych (tutaj int) oraz napisów (tutaj std::string) w C++

Jeśli świadomie chcemy oznaczyć, że istnieje możliwość nie używania danej zmiennej, to atrybut [[maybe_unused]] powinien zapewnić nam brak ostrzeżeń kompilatora (rysunek 2.21.2).

Rysunek 2.21.2. Przykładowe zastosowanie atrybutu [[maybe_unused]] dla zmiennej w języku C++

2.4.2. Stałe

Opisane wcześniej zmienne zgodnie ze swoją nazwą mogły być modyfikowane (ich wartość mogła być zmieniana). Jednak w kodzie programu następuje czasem konieczność zablokowania możliwości modyfikacji określonej zmiennej. Aby to wykonać można zastosować słowo kluczowe const.


Przykładowy zapis jest następujący:

const nazwa-typu nazwa-zmiennej = wartość;


Zatem definicja stałej liczbowej (typ int) może wyglądać:

const int zmienna = 512;


Stałe można też tworzyć za pomocą dyrektywy preprocesora #define (wcześniej poznaliśmy #include).


Przykładowy zapis stałej za pomocą #define:

#define INFECTED_FILES 1024


Jedną z różnic pomiędzy słowem kluczowym const, a dyrektywą #define jest to, że za pomocą preprocesora (#define) nasze nazwy stałych są po prostu podmieniane na wartości. Natomiast tworząc stałą za pomocą const możemy oczekiwać, że będzie ta wartość w pamięci programu w sposób podobny jak zmienna. Najprościej: będzie to zmienna w pamięci, której nie wolno modyfikować.


Przykład dla tworzenia stałych znajduje się na rysunku 2.22. Warto zauważyć, że na końcu programu (przy instrukcji return) jest użyta stała stworzona za pomocą #define, ale nie przez nas. Znajduje się ona w dołączanym pliku nagłówkowym, a my jako programiści możemy jej używać.

Rysunek 2.22. Przykład tworzenia stałych w C++ za pomocą słowa kluczowego const oraz dyrektywy preprocesora #define

2.4.3. Zakresy zmiennych

Zmienne mogą być definiowane w różnych miejscach w programie — w różnych, ale nie wszędzie. Dawno temu panowała praktyka, aby wszystkie planowane do użycia zmienne były na górze kodu. Jednak język C++ pozwala definiować zmienne nawet pomiędzy instrukcjami i można umieszczać je jak najbliżej miejsca w którym będą używane. Oczywiście przy wszystkim należy zachowywać rozsądek i nie zaburzać czytelności kodu.

Pracując aktualnie nad jedno-plikowym programem, który jest zawarty w main. cpp możemy umieszczać zmienne wewnątrz funkcji main() oraz nad nią. Te umieszczone w ciele funkcji nazywane są zmiennymi lokalnymi, a te nad — globalnymi.

Zmienne globalne będą widoczne i dostępne z całego programu, a te wewnątrz funkcji main() tylko w środku funkcji.

Zawsze trzeba dobrze przemyśleć i ograniczać tworzenie zmiennych globalnych. Nie powinno się dać modyfikować danych używanych do określonego celu z różnych miejsc programu. Słowo klucz: enkapsulacja (nazywana też hermetyzacja, kapsułkowanie).

Przykładowy kod w języku C++ dotyczący zakresu zmiennych przedstawia rysunek 2.23. Pokazano tam zmienną globalną, lokalną, dostęp do zmiennej globalnej operatorem zakresu (dwa dwukropki) oraz ograniczenie zakresu zmiennej poprzez stworzenie bloku klamrami { oraz }.

Rysunek 2.23. Przykład dot. zakresu (ang. scope) zmiennych w programie C++

2.5. Typy podstawowe

W poprzednich rozdziałach wspominane było jedynie o typie liczbowym (int) oraz napisach (std::string). W kolejnych podrozdziałach przedstawiono podstawowe typy danych udostępniane programistom C++.

2.5.1. Inicjalizacja

Pojęcie inicjalizacji zmiennej można rozumieć jako nadanie jej początkowej wartości. Na kodzie z rysunku 2.24 zaprezentowano trzy sposoby nadawania początkowej wartości zmiennym typu int (liczba całkowita) oraz std::string (napis). Sposób z użyciem znaku równości (operatora przypisania) jest dawną metodą stosowaną jeszcze w języku C. Metoda korzystająca z nawiasów działa niemalże identycznie dla typów podstawowych. Jest to zaznaczone w dokumencie Working Draft, Standard for Programming Language C++ w zdaniu „The form of initialization (using parentheses or =) is generally insignificant, but does matter when the entity being initialized has a class type.”


Pan Bjarne Stroustrup w swojej książce zaleca stosowanie inicjalizacji za pomocą nawiasów klamrowych, czyli {}. Jako główne zalety podaje, że wartości liczbowe nie będą uszkadzane, gdy zainicjalizuje się wartością większą od wartości jaką może maksymalnie przechowywać typ definiowanej zmiennej. Dodatkowo wartości zmiennoprzecinkowe (liczby rzeczywiste) nie będą zaokrąglane do liczb całkowitych, gdy zainicjalizuje się taką wartością zmienną całkowitą.

Rysunek 2.24. Różne sposoby inicjalizacji zmiennych w C++ na przykładzie typu int oraz std::string

2.5.2. Typy całkowitoliczbowe

Podstawowym typem do reprezentacji liczb całkowitych jest int. Standard języka C++ gwarantuje, że powinien on mieć rozmiar minimum 16 bitów. Bez modyfikatorów może on przechowywać liczby ze znakiem (ujemne lub dodatnie).

Rozmiar wybranego typu w bajtach można sobie wyświetlić fragmentem kodu:

std::cout << sizeof(int) << std::endl;

U mnie na Windows 10 64-bit wyświetliła się wartość cztery (4). Typy całkowitoliczbowe w języku C++ z dodatkowymi informacjami dotyczącymi odmiany Visual C++ przedstawiono na rysunku 2.25.


Możliwe jest zastosowanie słów modyfikujących zachowanie typu takich jak np. signed (pol. ze znakiem) czy unsigned (pol. bez znaku), czyli zapis signed int to typ int ze znakiem, a unsigned int to typ int bez znaku. Domyślnie pisząc tylko int otrzymuje się typ ze znakiem.

Visual C++ jest odmianą języka C++. Zawiera dodatkowe typy danych (przypis), oprócz tych, które definiuje standard języka C++. Odmiana C++ od Microsoft zawiera tak jak „czyste” C++ typ int, ale określa jego rozmiar w systemie Windows jako 32 bity (4 bajty). Nie minimum, tylko dokładnie tyle (sprawdziłem na Windows 10 x64). Standard C++ określa tylko, że int ma mieć minimum 16 bitów.


Dodatkowe typy, które nie są zawarte w standardzie języka C++ oznaczane są prefiksem __ (dwa znaki podkreślenia). W Visual C++ istnieją zatem typy całkowitoliczbowe np. __int8 (1 bajt), __int16 (2 bajty), __int32 (4 bajty), __int64 (8 bajtów).

Rysunek 2.25. Typy danych całkowitoliczbowe w języku C++

Dokładniejsze informacje o właściwościach typów arytmetycznych można uzyskać za pomocą klasy o nazwie numeric_limits z przestrzeni nazw std (rysunek 2.26).

Przykładowy kod przedstawiono na rysunku 2.26. Jako, że przedstawione źródło zawiera konstrukcje, które nie występowały wcześniej w książce, to niektóre fragmenty wymagają wyjaśnień. Linie 15 i 20 zawierają definicje stałych o nazwach byteMinbyteMax do których przypisywane są wartości z funkcji minmax, które znajdują się w klasie numeric_limits, a klasa ta zawarta jest w znanej przestrzeni nazw std (Jeszcze raz i trochę jaśniej — przyp. Mr. At).

Kod źródłowy na rysunku 2.26 dotyczy właściwości typów danych. Zaprezentowany przykład wyświetla zakres typu unsigned char, który w C++ można też nazwać bajtem. Zakres to przedział wartości, które określony typ może przechowywać. Bajt w C++ jest bez znaku i na architekturze x86/x64 ma minimalną wartość 0, a maksymalną 255 (widok konsoli na rysunku 2.26). Inne architektury („rodzaje komputerów”) mogą sobie ustalić np. aby bajt miał większy rozmiar.

Zakresy typów numerycznych są w nagłówku limits, dlatego w linii drugiej (2) dołączono ten plik dyrektywą #include <limits>. W liniach 15 do 18 oraz 20 do 23 definiowane są dwie stałe o nazwach byteMin oraz byteMax typu int. Będą one przechowywać pobrane wartości. Funkcja min() pobiera minimalną wartość, jaką można zapisać w bajcie (tutaj zero), a funkcja max( ) pobiera maksymalną wartość (tutaj 255). Twórcy nagłówka limits oznaczyli te funkcje jako constexpr, czyli wyrażenie, które prowadzi do stałego rezultatu i jest obliczane w czasie kompilacji (budowania programu — nie w czasie jego działania/wykonywania). Na przykładzie (rysunek 2.26) przypisujemy wynik działania wyrażenia stałego (funkcje min( )max( )) do stałych, zatem zalecane jest, aby te stałe też oznaczyć poprzez constexpr. Techniki tego typu pozwalają na szybsze wykonywanie się programu i oszczędzenie pamięci operacyjnej. Natomiast przed wywołaniami funkcji o nazwach min i max (linie 17 oraz 22, rysunek 2.26) można powiedzieć w prostych słowach, że uzyskujemy dostęp do tych funkcji. Nazwa przestrzeni std, dwa dwukropki i przeglądamy co potrzebujemy — o, jest klasa numeric_limits. Klikając prawym przyciskiem myszy w Visual Studio na nazwie i wybierając Przejdź do definicji można zauważyć słowo template. Zatem podajemy obok klasy w ostrych nawiasach (< oraz >) typ, tutaj BYTE. Dalej dwa dwukropki i przeglądamy co potrzebujemy — o, jest funkcja min. Nie przyjmuje argumentów, więc wywołanie z pustymi nawiasami.

Rysunek 2.26. Odczytywanie właściwości (np. zakres wartości) typów arytmetycznych z klasy std::numeric_limits w języku C++

2.5.3. Typy zmiennoprzecinkowe

Liczby rzeczywiste (zbiór liczb wymiernych i niewymiernych) można przechowywać w typach zmiennoprzecinkowych. Należy zaznaczyć, że wartość będzie miała określoną precyzję (dokładność).


Typy zmiennoprzecinkowe:

— float — pojedyncza precyzja

— double — podwójna precyzja

— long double — rozszerzona precyzja

Typ float w architekturze x86/x64 dla koprocesora x87 ma 32 bity, double ma 64 bity, a long double 80 bitów (przypis). Jednak w Visual C++ typ float ma 32 bity, double ma 64 bity, a long double też 64 bity.

Na rysunku 2.27 przedstawiono przykład dotyczący liczb zmiennoprzecinkowych.


Linia dziewiąta (9) — bez przyrostków liczba zmiennoprzecinkowa jest typu double.


Linia dziesiąta (10) — przyrostek f lub F określa, że liczba jest typu float.


Linia jedenasta (11) — przyrostek l lub L określa, że liczba jest typu long double.


Linia trzynasta (13) — przedrostek 0x określa, że liczba jest zapisana w systemie szesnastkowym (heksadecymalnym).


Linia czternasta (14) — apostrofami możemy oddzielać cyfry, a kompilator je pominie np. 100’000 (jest to dla czytelności).


Linia piętnasta (15) — dzielenie dwóch liczb double. Warto zaznaczyć, że jakbyśmy podzielili 1/2, a nie 1.0/2.0 to otrzymalibyśmy zero, gdyż nastąpiłoby zaokrąglenie.


Linia szesnasta (16) — obliczenie wartości pierwiastka z liczby dwa.


W kodzie na rysunku 2.27 linie od osiemnaście (18) do dwadzieścia jeden (21) to próba pobrania maksymalnej wartości dla typu long double na maszynie, gdzie uruchomimy program.

Na moim komputerze wyświetliła się wartość 1.79769e+308. Jest to notacja naukowa. Wartość ta jest równa 1.79769 × 10 do potęgi 308. Przyglądając się bardziej tej liczbie można zauważyć, że to też wartość maksymalna dla typu double. W pliku nagłówkowym float. h jest zapis, że wartość maksymalna dla double i long double jest taka sama. Fragment z pliku float. h:

#define LDBL_MAX DBL_MAX

Jest to zgodne ze standardem języka C++, gdyż ten dokument gwarantuje, że „typ double ma precyzje taką jak float lub większą, a typ long double ma precyzję taką jak double lub większą”.

Rysunek 2.22. Przykład dotyczący liczb zmiennoprzecinkowych dla języka C++

/* Wskazówka */

Jeśli nie chcemy na standardowym wyjściu notacji naukowej, to można użyć zapisu:

std::cout << std::fixed << myValue;


Natomiast aby osiągnąć zapis szesnastkowy (heksadecymalny) to można zastosować std::hex.

2.5.4. Typy znakowe

Jeśli potrzebujemy operować na pojedynczym znaku tekstu, możemy skorzystać z typów znakowych. Zwyczajnym typem jest char (od character — znak). Wymowa tego słowa to „tchar”, nie „kar” (przypis). Może on przechowywać podstawowy zakres znaków i być wartością ze znakiem (signed) lub bez znaku (unsigned), a jest to zależne od implementacji standardu języka C++. W celu możliwości przechowywania maksymalnego dostępnego zakresu znaków należy użyć wchar_t.


Na rysunku 2.28 przedstawiono przykładowy kod źródłowy z definicjami różnych typów znakowych.

Stałą znakową definiuje się pomiędzy apostrofami np. „A”. Możliwe jest też używanie przedrostków. Przedrostek u8 tworzy stałą typu char8_t, przedrostek u tworzy stałą typu char16_t, przedrostek U tworzy stałą typu char32_t, a przedrostek L tworzy stałą typu wchar_t.

Przy programowaniu za pomocą Windows API (dołączenie #include <Windows. h>) możliwe jest dla typu znakowego stosować nazwę TCHAR. Dzięki temu, zmieniając ustawienie używane w projekcie dot. znaków (Projekt > Właściwości z górnego menu) pomiędzy kodowaniem wielobajtowym, a Unicode, nazwa TCHAR będzie automatycznie zamieniać się na CHAR lub WCHAR.

Dodatkowo istnieje też nazwa BYTE, która jest zdefiniowana jako unsigned char, a używanie jej przy operacjach na bajtach zwiększa czytelność i łatwość zrozumienia kodu, gdy chodzi o bajt jako liczbę, a nie znak drukowalny.

Rysunek 2.28. Przykład dotyczący typów znakowych w C++

2.5.5. Typ logiczny

Tak jak bit może przechowywać tylko jedną z dwóch wartości (zero lub jeden), tak podobnie w języku C++ istnieje typ, którego wartością może być „prawda” (true) lub „fałsz” (false). Mimo, że typ logiczny przechowuje tylko jedną z dwóch wartości, to wewnątrz jest reprezentowany przez typ całkowity int bez znaku (rysunek 2.29). Jako częste zastosowanie typu logicznego można wymienić m.in.:

— fragment kodu, który zwraca dwa stany (np. plik istnieje lub nie),

— wynik operacji logicznych m.in. takich jak porównanie (np. zmienna „a” jest wyzerowana lub nie),

— i inne.

Rysunek 2.29. Typ logiczny w języku C++ (przykład definicji zmiennych bool oraz niejawna konwersja na unsigned int)

2.5.6. Typ wyliczeniowy

Słowo kluczowe enum pozwala zadeklarować tzw. wyliczenie. Jest to zbiór stałych wartości, którym chcemy nadać nazwę. Przykładami mogą być nazwy dni tygodnia, rodzaje produktu w sklepie czy inny zbiór, który można zgrupować. Zalecane jest jednak używanie klas wyliczeniowych (enum class), zamiast zwykłych wyliczeń. Niektórymi z powodów są m.in., że zwykłe wyliczenia nie mają zakresu i zaśmiecają przestrzeń nazw. Np. gdy mamy fragment kodu dotyczący bazy danych, to nasze wyliczenia nie powinny być widoczne we fragmencie kodu, który wyświetla grafikę na ekranie. Klasy wyliczeniowe natomiast posiadają zakres i przy odwoływaniu się do zawartych w nich wartości używamy operatora zakresu (dwa dwukropki). Powoduje to, że wymusza to podanie z jakiej klasy wyliczeniowej jest dana wartość.

Na rysunku 2.30 przedstawiono jak deklarować klasy wyliczeniowe z przykładowym zbiorem wartości. Natomiast w funkcji głównej main() zaprezentowano definiowanie zmiennych, które są typu danej klasy wyliczeniowej wraz z nadaniem im początkowej wartości. Przykład z rysunku 2.30 nawiązuje do prototypu fragmentu wirusa komputerowego, w którym zdefiniowane są określone ładunki (ang. payload), czyli kody do wykonania, które zmyślony wirus miałby przenosić i detonować. Pod stałymi w klasie wyliczeniowej ukryte są wartości liczbowe. Jeśli nie zrobimy np.:

enum class Color { green = 1, blue = 7 };

To wartości schowane pod nazwami będą automatycznie numerowane od zera i zwiększane o jeden.

Rysunek 2.30. Typ wyliczeniowy (klasa wyliczeniowa) w C++

2.5.7. Typ void

Niekompletny typ void w języku C++ charakteryzuje się m.in. tym, że nie można tworzyć obiektów o takim typie (rysunek 2.31). Potrzebny jest jednak w innych sytuacjach. Powtarzająca się chyba w każdym przykładzie funkcja główna main() zgodnie ze standardem języka C++ zwraca typ int (liczba całkowita ze znakiem), ale istnieją takie funkcje (i sami możemy też je tworzyć), które nie potrzebują zwracać żadnej wartości. Wtedy nagłówek funkcji zawiera słowo kluczowe void, czyli np.

void function5( ) { };


Kolejnym przykładem zastosowania typu void są wskaźniki. Na tym etapie można je rozumieć jako obiekt, który jest odwołaniem (inaczej: wskazanie, adres w pamięci) do innego obiektu. Jeśli nie znamy typu obiektu do którego tworzymy wskaźnik, to można zastosować typ void.

Rysunek 2.31. Typ danych void jest nazywany niekompletnym i obiekty typu void są niedozwolone w języku C++

2.6. Definiowanie własnych nazw typów

Słowo kluczowe using pozwala tworzyć aliasy (inne słowo: synonimy) dla istniejących typów (rysunek 2.32). Należy zaznaczyć, że nie jest tutaj tworzony nowy typ (jak w opisanej wcześniej klasie wyliczeniowej czy opisanej dalej klasie jako typie użytkownika). Przykładowe zastosowanie: nadajemy nową nazwę określonemu typowi danych. Dzięki temu później, gdy potrzebujemy zmienić używany typ, to nie zmieniamy tej nazwy w każdym miejscu w kodzie, gdzie ta nazwa występuje, tylko dokonujemy modyfikacji przy słowie using.

Inne przydatne zastosowanie to stworzenie ładniejszej czy krótszej nazwy dla wskaźnika do funkcji np.:

using MyVoidFunction = void(*)( );

Prawda, że nazwa MyVoidFunction czytelniej wygląda, niż te nawiasy i gwiazdka? Wskaźniki są opisane w dalszych rozdziałach.

Rysunek 2.32. Tworzenie aliasów dla istniejących typów w języku C++ za pomocą słowa using

2.7. Dedukcja typu

Przeczytałeś bezpłatny fragment.
Kup książkę, aby przeczytać do końca.
E-book
za 27.03
drukowana A5
za 49.97
drukowana A5
Kolorowa
za 80.59