Microsoft Visual Studio 2012. Programowanie w C i C++

Dołącz do najlepszych, zacznij programować w C i C++! Poznaj środowisko Microsoft Visual Studio 2012! Odkryj uniwersalne

991 146 10MB

Polish Pages [591] Year 2013

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Microsoft Visual Studio 2012. Programowanie w C i C++

Table of contents :
Spis treści
Wstęp
Język C
Język C++
Zawartość książki
Trochę teorii
Kod maszynowy
Asembler
Interpreter
Kompilator
Kompilator JIT
Problem jajka i kury
Część I Środowisko pracy
Rozdział 1. Środowisko programistyczne Microsoft Visual Studio Express 2012
Pobranie programu instalacyjnego
Instalacja
Pierwsze uruchomienie
Instalacja pakietu polonizującego
Uruchamianie
Wybór języka interfejsu użytkownika
Rejestracja
Tworzenie nowego projektu
Tworzenie zbiorów tekstu źródłowego programu
Kompilacja rozwiązania
Wybór wariantu kompilacji
Usuwanie błędów kompilacji
Zapisywanie zmian w tekście źródłowym
Uruchamianie programu spoza środowiska
Kończenie pracy
Część II Język C
Rozdział 2. Wprowadzanie, wyprowadzanie i przechowywanie danych
Podprogram główny
Umieszczanie tekstu na ekranie
Komentarze
Przechowywanie danych w pamięci
Literały
Deklaracja a definicja
Definiowanie zmiennej
Całkowite typy danych
Znakowe typy danych
Zmiennoprzecinkowe typy danych
Nazywanie zmiennych
Deklarowanie własnych typów danych
Obliczenia
Operacje algebraiczne
Operacje bitowe
Skracanie wyrażeń algebraicznych
Wyprowadzanie danych
Rzutowanie typów danych
Zmienne tymczasowe
Pobieranie informacji o zmiennych
Pobieranie danych z klawiatury
Śledzenie realizacji programu
Krokowa realizacja programu
Podgląd zawartości zmiennych
Pułapki
Podsumowanie
Rozdział 3. Konstrukcje warunkowe i typy wyliczeniowe
Zmienne i wyrażenia logiczne
Wartości logiczne
Zmienne logiczne
Wyrażenia logiczne
Instrukcja warunkowa
Operator warunkowy
Typy wyliczeniowe
Definiowanie typu wyliczeniowego
Wartości liczbowe wyliczeń
Używanie zmiennych typów wyliczeniowych
Konwersja typów wyliczeniowych i liczbowych
Konstrukcja warunkowa
Podsumowanie
Rozdział 4. Struktury i unie
Struktury
Deklarowanie struktury
Definiowanie zmiennej strukturalnej
Korzystanie z pól struktury
Zagnieżdżanie struktur
Kopiowanie zmiennych strukturalnych
Unie
Deklarowanie unii
Definiowanie unii
Korzystanie z pól unii
Unie anonimowe w strukturach
Podsumowanie
Rozdział 5. Pętle, tablice i zmienne łańcuchowe
Pętle
Pętla while
Pętla do…while
Pętla for
Przerywanie pętli
Tablice
Definicja tablicy
Zajętość pamięci tablicy
Dostęp do elementów tablicy
Tablice wielowymiarowe
Zerowanie istniejącej tablicy
Zmienne łańcuchowe
Definiowanie zmiennej łańcuchowej
Wypisywanie tekstu na ekranie
Generowanie tekstu
Przypisywanie i kopiowanie tekstu
Scalanie napisów
Wprowadzanie tekstu z klawiatury
Pomiar długości tekstu
Porównywanie napisów
Wyszukiwanie fragmentów tekstu
Podprogramy standardowe operujące na znakach
Podsumowanie
Rozdział 6. Programowanie proceduralne
Techniki podziału na podprogramy
Podprogramy bezparametrowe
Zmienne globalne i lokalne
Zmienne lokalne
Zmienne globalne
Statyczne zmienne lokalne
Parametry podprogramów
Parametry typów prostych
Parametry tablicowe
Parametry typów złożonych
Parametry typu łańcuchowego
Zwracanie wyniku działania podprogramu
Zwracanie wyniku przez return
Zwracanie wyników przez parametry
Deklaracje podprogramów
Podprogramy biblioteki standardowej
stdlib.h
math.h
time.h
conio.h
Rekurencja
Rekurencja jako odpowiednik pętli
Algorytmy rekurencyjne
Stos wywołań podprogramów
Program przykładowy
Podsumowanie
Rozdział 7. Operacje wejścia-wyjścia
Terminologia
Zbiory danych
Strumień danych
Strumienie standardowe
Wejście i wyjście standardowe
Podprogramy wprowadzania i wyprowadzania danych
Uruchamianie programów wsadowych
Potoki
Filtry
Współpraca z systemem operacyjnym
Wiersz polecenia
Ustalanie treści wiersza polecenia
Kod wyniku działania programu
Zbiory
Zmienna plikowa
Otwieranie zbioru dyskowego
Zamykanie zbioru dyskowego
Zapisywanie danych tekstowych
Odczytywanie danych tekstowych
Stwierdzanie osiągnięcia końca zbioru
Powtórne odczytanie zbioru
Przykład odczytywania informacji ze zbioru tekstowego
Binarny zapis informacji
Zapisywanie danych binarnych
Odczytywanie danych binarnych
Zmienne łańcuchowe w zbiorach binarnych
Pozycja w zbiorze binarnym
Pobieranie rozmiaru zbioru
Przykład korzystania ze zbioru binarnego
Opróżnianie buforów zapisu
Podsumowanie
Rozdział 8. Dynamiczne gospodarowanie pamięcią operacyjną
Informacje podstawowe
Adresacja pamięci
Typy bloków pamięci
Terminologia
Zmienne wskaźnikowe
Definicja
Pobranie adresu zmiennej
Amorficzna zmienna wskaźnikowa
Dereferencja
Zmiana przyporządkowania
Zmienne wskaźnikowe a tablice
Zmienne wskaźnikowe a struktury i unie
Zmienne wskaźnikowe a zmienne łańcuchowe
Rozmiar wskaźnika
Pamięć przydzielana dynamicznie
Alokowanie bloku pamięci
Zmiana rozmiaru bloku pamięci
Zwalnianie bloku pamięci
Dynamiczna alokacja pamięci na stosie
Operacje na blokach pamięci
Zapełnianie bloku pamięci
Kopiowanie zawartości bloku pamięci
Porównywanie zawartości bloków pamięci
Wyszukiwanie bajtu
Podsumowanie
Rozdział 9. Programowanie wielomodułowe
Jednostki kompilacji
Dodawanie nowej jednostki kompilacji
Zmiana nazwy zbioru
Usuwanie jednostki kompilacji
Podprogramy
Deklaracje podprogramów
W różnych jednostkach kompilacji
Zmienne globalne
Zbiory nagłówkowe
Kompilacja warunkowa
Warianty generowania kodu
Kompilacja warunkowa
Asercje
Podsumowanie
Część III Język C++
Rozdział 10. Składnia języka C++
Zbiory nagłówkowe
Zmienne
Definiowanie zmiennych
Typy złożone
Atrybut const
Wartości stałe
Referencje
Podprogramy
Parametry typu const
Parametry przekazywane przez referencję
Przeciążanie podprogramów
Argumenty domyślne
Rzutowanie typów danych
Podsumowanie
Rozdział 11. Przestrzenie nazw
Tworzenie i rozszerzanie przestrzeni nazw
Używanie przestrzeni nazw
Wymuszanie użycia przestrzeni nazw
Używanie wybranych symboli
Używanie przestrzeni nazw
Przemianowywanie przestrzeni nazw
Anonimowe przestrzenie nazw
Podsumowanie
Rozdział 12. Programowanie obiektowe
Struktury danych
Deklaracja struktury
Pola statyczne
Konstruktor
Destruktor
Atrybuty dostępności
Metody
Wskaźnik this
Hermetyzacja struktur danych
Struktury, klasy, obiekty
Podsumowanie
Rozdział 13. Dziedziczenie i polimorfizm
Dziedziczenie
Klasa bazowa
Klasa pochodna
Hierarchia klas
Dziedziczenie a zawieranie
Konstruktory klas pochodnych
Destruktory klas pochodnych
Przykrywanie metod
Dziedziczenie wielobazowe
Przykład
Polimorfizm w dziedziczeniu
Kiedy dziedziczenie i przykrywanie metod nie wystarczają
Metody wirtualne
Konstruktory i destruktory
Dynamiczna identyfikacja typów
Mechanizm RTTI
Rzutowanie dynamiczne
Uwagi
Podsumowanie
Rozdział 14. Przeciążanie operatorów
Operatory
Przeciążanie operatorów symbolicznych
Binarne operatory arytmetyczne
Unarne operatory arytmetyczne
Operatory porównania
Operatory logiczne
Operatory bitowe
Operator przypisania
Operatory zespolone
Operator indeksowy
Operatory adresowe
Pozostałe operatory
Operatory konwersji
Podsumowanie
Rozdział 15. Programowanie uogólnione
Szablony podprogramów
Zapisywanie szablonu
Wyszczególnienie szablonu
Specjalizowanie szablonu
Szablony klas
Szablony metod
Zapisywanie szablonu
Wyszczególnienie szablonu
Szablony zależne od wartości liczbowych
Argumenty domyślne szablonu
Specjalizowanie szablonu
Zaawansowane szablony
Szablony cechujące
Klasy określające reguły
Podsumowanie
Rozdział 16. Zmienne łańcuchowe i strumienie
Zmienne łańcuchowe
Obiektowe typy łańcuchowe
Tworzenie zmiennej łańcuchowej
Zmiana zawartości zmiennej łańcuchowej
Pobieranie długości tekstu
Pobieranie treści
Porównywanie tekstu
Dostęp do pojedynczych znaków
Wyszukiwanie fragmentów tekstu
Pobieranie wycinka tekstu
Strumienie standardowe
Podstawowe klasy strumieni
Strumienie standardowe
Operatory strumieniowe
Wyprowadzanie informacji
Formatowanie wyprowadzanych danych
Wprowadzanie danych
Wykrywanie końca danych wejściowych
Strumienie zbiorów pamięci masowej
Tryby otwarcia zbioru
Otwieranie zbioru
Zamykanie zbioru
Serializacja i deserializacja danych
Zbiory binarne
Strumienie wirtualne
Wirtualny strumień wejściowy
Wirtualny strumień wyjściowy
Podsumowanie
Rozdział 17. Obsługa sytuacji wyjątkowych
Sytuacje wyjątkowe
Opis obiektowy
Terminologia
Wydajność
Standardowe klasy sytuacji wyjątkowych
Obsługa sytuacji wyjątkowych
Blok chroniony
Zgłaszanie sytuacji wyjątkowej
Blok obsługi sytuacji wyjątkowych
Zmienne w bloku chronionym
Specyfikacje sytuacji wyjątkowych
Specyfikacja sytuacji wyjątkowej
Specyfikacja pusta
Specyfikacja noexcept
Podsumowanie
Rozdział 18. Iteratory
Teoria iteratorów
Postać iteratora
Rodzaje iteratorów
Operacje wykonywane na iteratorze
Iteratory standardowe
Szablon std::iterator
Zwyczajowe nazwy klas iteratorów
Pobieranie iteratorów
Przemieszczanie iteratora
Pomiar odległości iteratorów
Własne klasy iteratorów
Przykładowa klasa kontenera
Klasa iteratora
Pola i ich inicjalizacja
Przeciążenie operatorów
Metody tworzące iteratory
Całość klasy kontenera i iteratora
Iterowanie kolekcji z wykorzystaniem iteratorów
Przemieszczenie i odległość
Program przykładowy
Podsumowanie
Rozdział 19. Biblioteka standardowa STL
Pojęcia podstawowe
Alokatory
Pary
Kontenery
Podstawowe struktury przechowywania danych
Podstawowe operacje
Tablica dynamiczna
Tablica segmentowa
Lista
Adaptery
Stos
Kolejka
Tablica asocjacyjna
Zbiory elementów
Algorytmy
Zamiana wartości
Zliczanie elementów
Określanie wartości skrajnych
Porównywanie elementów
Kopiowanie elementów
Zapełnianie podzbioru elementów
Porządkowanie elementów
Podsumowanie
Rozdział 20. Programowanie wielomodułowe
Przestrzenie nazw
Klasy
Szablony
Podsumowanie
Dodatek Tabela kodów znaków ASCII
Skorowidz

Citation preview

Spis treści Wstęp ............................................................................................ 11 Język C ........................................................................................................................... 11 Język C++ ....................................................................................................................... 12 Zawartość książki ........................................................................................................... 14 Trochę teorii ................................................................................................................... 15 Kod maszynowy .................................................................................................... 15 Asembler ............................................................................................................... 16 Interpreter .............................................................................................................. 16 Kompilator ............................................................................................................ 17 Kompilator JIT ...................................................................................................... 18 Problem jajka i kury .............................................................................................. 18

Część I

Środowisko pracy ......................................................... 19

Rozdział 1. Środowisko programistyczne Microsoft Visual Studio Express 2012 ............................................ 21 Pobranie programu instalacyjnego .................................................................................. 21 Instalacja ......................................................................................................................... 24 Pierwsze uruchomienie ................................................................................................... 26 Instalacja pakietu polonizującego ................................................................................... 28 Uruchamianie ................................................................................................................. 30 Wybór języka interfejsu użytkownika ............................................................................ 32 Rejestracja ...................................................................................................................... 33 Tworzenie nowego projektu ........................................................................................... 35 Tworzenie zbiorów tekstu źródłowego programu ........................................................... 38 Kompilacja rozwiązania ................................................................................................. 42 Wybór wariantu kompilacji ............................................................................................ 44 Usuwanie błędów kompilacji ......................................................................................... 45 Zapisywanie zmian w tekście źródłowym ...................................................................... 48 Uruchamianie programu spoza środowiska .................................................................... 48 Kończenie pracy ............................................................................................................. 49

Część II

Język C ........................................................................ 51

Rozdział 2. Wprowadzanie, wyprowadzanie i przechowywanie danych ................ 53 Podprogram główny ....................................................................................................... 53 Umieszczanie tekstu na ekranie ...................................................................................... 55

4

Spis treści Komentarze .................................................................................................................... 61 Przechowywanie danych w pamięci ............................................................................... 63 Literały .................................................................................................................. 63 Deklaracja a definicja ............................................................................................ 63 Definiowanie zmiennej ......................................................................................... 65 Całkowite typy danych .......................................................................................... 65 Znakowe typy danych ........................................................................................... 68 Zmiennoprzecinkowe typy danych ........................................................................ 69 Nazywanie zmiennych .......................................................................................... 70 Deklarowanie własnych typów danych ................................................................. 71 Obliczenia ....................................................................................................................... 71 Operacje algebraiczne ........................................................................................... 71 Operacje bitowe .................................................................................................... 73 Skracanie wyrażeń algebraicznych ........................................................................ 74 Wyprowadzanie danych ................................................................................................. 77 Rzutowanie typów danych .............................................................................................. 81 Zmienne tymczasowe ..................................................................................................... 82 Pobieranie informacji o zmiennych ................................................................................ 83 Pobieranie danych z klawiatury ...................................................................................... 86 Śledzenie realizacji programu ......................................................................................... 90 Krokowa realizacja programu ............................................................................... 91 Podgląd zawartości zmiennych ............................................................................. 92 Pułapki .................................................................................................................. 94 Podsumowanie ................................................................................................................ 94

Rozdział 3. Konstrukcje warunkowe i typy wyliczeniowe ................................... 97 Zmienne i wyrażenia logiczne ........................................................................................ 98 Wartości logiczne .................................................................................................. 98 Zmienne logiczne .................................................................................................. 98 Wyrażenia logiczne ............................................................................................... 99 Instrukcja warunkowa ................................................................................................... 102 Operator warunkowy .................................................................................................... 106 Typy wyliczeniowe ...................................................................................................... 109 Definiowanie typu wyliczeniowego .................................................................... 109 Wartości liczbowe wyliczeń ................................................................................ 110 Używanie zmiennych typów wyliczeniowych .................................................... 111 Konwersja typów wyliczeniowych i liczbowych ................................................ 112 Konstrukcja warunkowa ............................................................................................... 113 Podsumowanie .............................................................................................................. 120

Rozdział 4. Struktury i unie ............................................................................ 123 Struktury ....................................................................................................................... 124 Deklarowanie struktury ....................................................................................... 125 Definiowanie zmiennej strukturalnej ................................................................... 126 Korzystanie z pól struktury ................................................................................. 127 Zagnieżdżanie struktur ........................................................................................ 129 Kopiowanie zmiennych strukturalnych ............................................................... 133 Unie .............................................................................................................................. 134 Deklarowanie unii ............................................................................................... 135 Definiowanie unii ................................................................................................ 135 Korzystanie z pól unii ......................................................................................... 135 Unie anonimowe w strukturach ........................................................................... 136 Podsumowanie .............................................................................................................. 138

Spis treści

5

Rozdział 5. Pętle, tablice i zmienne łańcuchowe ............................................. 141 Pętle .............................................................................................................................. 142 Pętla while ........................................................................................................... 143 Pętla do…while ................................................................................................... 146 Pętla for ............................................................................................................... 149 Przerywanie pętli ................................................................................................. 151 Tablice .......................................................................................................................... 154 Definicja tablicy .................................................................................................. 154 Zajętość pamięci tablicy ...................................................................................... 156 Dostęp do elementów tablicy .............................................................................. 157 Tablice wielowymiarowe .................................................................................... 158 Zerowanie istniejącej tablicy ............................................................................... 160 Zmienne łańcuchowe .................................................................................................... 161 Definiowanie zmiennej łańcuchowej ................................................................... 163 Wypisywanie tekstu na ekranie ........................................................................... 164 Generowanie tekstu ............................................................................................. 166 Przypisywanie i kopiowanie tekstu ..................................................................... 167 Scalanie napisów ................................................................................................. 171 Wprowadzanie tekstu z klawiatury ..................................................................... 172 Pomiar długości tekstu ........................................................................................ 175 Porównywanie napisów ....................................................................................... 177 Wyszukiwanie fragmentów tekstu ...................................................................... 179 Podprogramy standardowe operujące na znakach ............................................... 183 Podsumowanie .............................................................................................................. 184

Rozdział 6. Programowanie proceduralne ........................................................ 187 Techniki podziału na podprogramy .............................................................................. 188 Podprogramy bezparametrowe ..................................................................................... 189 Zmienne globalne i lokalne .......................................................................................... 193 Zmienne lokalne .................................................................................................. 194 Zmienne globalne ................................................................................................ 195 Statyczne zmienne lokalne .................................................................................. 198 Parametry podprogramów ............................................................................................ 199 Parametry typów prostych ................................................................................... 200 Parametry tablicowe ............................................................................................ 201 Parametry typów złożonych ................................................................................ 203 Parametry typu łańcuchowego ............................................................................ 206 Zwracanie wyniku działania podprogramu ................................................................... 207 Zwracanie wyniku przez return ........................................................................... 208 Zwracanie wyników przez parametry .................................................................. 209 Deklaracje podprogramów ............................................................................................ 211 Podprogramy biblioteki standardowej .......................................................................... 212 stdlib.h ................................................................................................................. 212 math.h .................................................................................................................. 213 time.h .................................................................................................................. 214 conio.h ................................................................................................................. 215 Rekurencja .................................................................................................................... 215 Rekurencja jako odpowiednik pętli ..................................................................... 216 Algorytmy rekurencyjne ..................................................................................... 217 Stos wywołań podprogramów ............................................................................. 218 Program przykładowy ................................................................................................... 220 Podsumowanie .............................................................................................................. 225

6

Spis treści

Rozdział 7. Operacje wejścia-wyjścia .............................................................. 227 Terminologia ................................................................................................................ 228 Zbiory danych ..................................................................................................... 228 Strumień danych .................................................................................................. 230 Strumienie standardowe ...................................................................................... 231 Wejście i wyjście standardowe ..................................................................................... 231 Podprogramy wprowadzania i wyprowadzania danych ...................................... 232 Uruchamianie programów wsadowych ............................................................... 233 Potoki .................................................................................................................. 236 Filtry .................................................................................................................... 239 Współpraca z systemem operacyjnym .......................................................................... 241 Wiersz polecenia ................................................................................................. 241 Ustalanie treści wiersza polecenia ....................................................................... 242 Kod wyniku działania programu ......................................................................... 243 Zbiory ........................................................................................................................... 245 Zmienna plikowa ................................................................................................. 245 Otwieranie zbioru dyskowego ............................................................................. 245 Zamykanie zbioru dyskowego ............................................................................. 247 Zapisywanie danych tekstowych ......................................................................... 248 Odczytywanie danych tekstowych ...................................................................... 249 Stwierdzanie osiągnięcia końca zbioru ................................................................ 250 Powtórne odczytanie zbioru ................................................................................ 251 Przykład odczytywania informacji ze zbioru tekstowego .................................... 252 Binarny zapis informacji ..................................................................................... 254 Zapisywanie danych binarnych ........................................................................... 255 Odczytywanie danych binarnych ........................................................................ 257 Zmienne łańcuchowe w zbiorach binarnych ....................................................... 258 Pozycja w zbiorze binarnym ............................................................................... 261 Pobieranie rozmiaru zbioru ................................................................................. 263 Przykład korzystania ze zbioru binarnego ........................................................... 264 Opróżnianie buforów zapisu ............................................................................... 269 Podsumowanie .............................................................................................................. 270

Rozdział 8. Dynamiczne gospodarowanie pamięcią operacyjną ........................ 273 Informacje podstawowe ................................................................................................ 274 Adresacja pamięci ............................................................................................... 274 Typy bloków pamięci .......................................................................................... 276 Terminologia ....................................................................................................... 277 Zmienne wskaźnikowe ................................................................................................. 278 Definicja .............................................................................................................. 278 Pobranie adresu zmiennej .................................................................................... 279 Amorficzna zmienna wskaźnikowa ..................................................................... 280 Dereferencja ........................................................................................................ 281 Zmiana przyporządkowania ................................................................................ 282 Zmienne wskaźnikowe a tablice .......................................................................... 283 Zmienne wskaźnikowe a struktury i unie ............................................................ 284 Zmienne wskaźnikowe a zmienne łańcuchowe ................................................... 285 Rozmiar wskaźnika ............................................................................................. 286 Pamięć przydzielana dynamicznie ................................................................................ 288 Alokowanie bloku pamięci .................................................................................. 289 Zmiana rozmiaru bloku pamięci .......................................................................... 292 Zwalnianie bloku pamięci ................................................................................... 295 Dynamiczna alokacja pamięci na stosie .............................................................. 298

Spis treści

7 Operacje na blokach pamięci ........................................................................................ 299 Zapełnianie bloku pamięci .................................................................................. 299 Kopiowanie zawartości bloku pamięci ................................................................ 302 Porównywanie zawartości bloków pamięci ......................................................... 307 Wyszukiwanie bajtu ............................................................................................ 310 Podsumowanie .............................................................................................................. 311

Rozdział 9. Programowanie wielomodułowe .................................................... 313 Jednostki kompilacji ..................................................................................................... 314 Dodawanie nowej jednostki kompilacji .............................................................. 314 Zmiana nazwy zbioru .......................................................................................... 316 Usuwanie jednostki kompilacji ........................................................................... 317 Podprogramy ................................................................................................................ 317 Deklaracje podprogramów .................................................................................. 318 W różnych jednostkach kompilacji ..................................................................... 320 Zmienne globalne ......................................................................................................... 321 Zbiory nagłówkowe ...................................................................................................... 322 Kompilacja warunkowa ................................................................................................ 323 Warianty generowania kodu ................................................................................ 323 Kompilacja warunkowa ....................................................................................... 324 Asercje ................................................................................................................ 325 Podsumowanie .............................................................................................................. 327

Część III Język C++ .................................................................. 329 Rozdział 10. Składnia języka C++ ..................................................................... 331 Zbiory nagłówkowe ...................................................................................................... 331 Zmienne ........................................................................................................................ 332 Definiowanie zmiennych ..................................................................................... 332 Typy złożone ....................................................................................................... 333 Atrybut const ....................................................................................................... 333 Wartości stałe ...................................................................................................... 334 Referencje ........................................................................................................... 335 Podprogramy ................................................................................................................ 338 Parametry typu const ........................................................................................... 338 Parametry przekazywane przez referencję .......................................................... 339 Przeciążanie podprogramów ............................................................................... 340 Argumenty domyślne .......................................................................................... 342 Rzutowanie typów danych ............................................................................................ 343 Podsumowanie .............................................................................................................. 345

Rozdział 11. Przestrzenie nazw ......................................................................... 347 Tworzenie i rozszerzanie przestrzeni nazw .................................................................. 348 Używanie przestrzeni nazw .......................................................................................... 349 Wymuszanie użycia przestrzeni nazw .......................................................................... 351 Używanie wybranych symboli ............................................................................ 351 Używanie przestrzeni nazw ................................................................................. 352 Przemianowywanie przestrzeni nazw .................................................................. 352 Anonimowe przestrzenie nazw ..................................................................................... 353 Podsumowanie .............................................................................................................. 354

Rozdział 12. Programowanie obiektowe ............................................................ 355 Struktury danych .......................................................................................................... 355 Deklaracja struktury ............................................................................................ 355 Pola statyczne ...................................................................................................... 356

8

Spis treści Konstruktor ......................................................................................................... 357 Destruktor ............................................................................................................ 367 Atrybuty dostępności ........................................................................................... 371 Metody ................................................................................................................ 373 Wskaźnik this ...................................................................................................... 381 Hermetyzacja struktur danych ...................................................................................... 382 Struktury, klasy, obiekty ............................................................................................... 387 Podsumowanie .............................................................................................................. 388

Rozdział 13. Dziedziczenie i polimorfizm ........................................................... 391 Dziedziczenie ............................................................................................................... 392 Klasa bazowa ...................................................................................................... 392 Klasa pochodna ................................................................................................... 393 Hierarchia klas .................................................................................................... 394 Dziedziczenie a zawieranie ................................................................................. 394 Konstruktory klas pochodnych ............................................................................ 396 Destruktory klas pochodnych .............................................................................. 398 Przykrywanie metod ............................................................................................ 399 Dziedziczenie wielobazowe ................................................................................ 401 Przykład .............................................................................................................. 407 Polimorfizm w dziedziczeniu ....................................................................................... 411 Kiedy dziedziczenie i przykrywanie metod nie wystarczają ............................... 411 Metody wirtualne ................................................................................................ 413 Konstruktory i destruktory .................................................................................. 423 Dynamiczna identyfikacja typów ................................................................................. 424 Mechanizm RTTI ................................................................................................ 424 Rzutowanie dynamiczne ..................................................................................... 426 Uwagi .................................................................................................................. 428 Podsumowanie .............................................................................................................. 428

Rozdział 14. Przeciążanie operatorów ............................................................... 431 Operatory ...................................................................................................................... 432 Przeciążanie operatorów symbolicznych ...................................................................... 433 Binarne operatory arytmetyczne .......................................................................... 433 Unarne operatory arytmetyczne .......................................................................... 434 Operatory porównania ......................................................................................... 436 Operatory logiczne .............................................................................................. 437 Operatory bitowe ................................................................................................. 438 Operator przypisania ........................................................................................... 438 Operatory zespolone ............................................................................................ 440 Operator indeksowy ............................................................................................ 440 Operatory adresowe ............................................................................................. 442 Pozostałe operatory ............................................................................................. 442 Operatory konwersji ..................................................................................................... 443 Podsumowanie .............................................................................................................. 444

Rozdział 15. Programowanie uogólnione ........................................................... 445 Szablony podprogramów .............................................................................................. 446 Zapisywanie szablonu ......................................................................................... 447 Wyszczególnienie szablonu ................................................................................ 448 Specjalizowanie szablonu .................................................................................... 450 Szablony klas ................................................................................................................ 451 Szablony metod ................................................................................................... 452 Zapisywanie szablonu ......................................................................................... 453 Wyszczególnienie szablonu ................................................................................ 454

Spis treści

9 Szablony zależne od wartości liczbowych .......................................................... 455 Argumenty domyślne szablonu ........................................................................... 456 Specjalizowanie szablonu .................................................................................... 457 Zaawansowane szablony .............................................................................................. 460 Szablony cechujące ............................................................................................. 460 Klasy określające reguły ..................................................................................... 461 Podsumowanie .............................................................................................................. 465

Rozdział 16. Zmienne łańcuchowe i strumienie ................................................. 467 Zmienne łańcuchowe .................................................................................................... 468 Obiektowe typy łańcuchowe ............................................................................... 468 Tworzenie zmiennej łańcuchowej ....................................................................... 470 Zmiana zawartości zmiennej łańcuchowej .......................................................... 470 Pobieranie długości tekstu ................................................................................... 471 Pobieranie treści .................................................................................................. 471 Porównywanie tekstu .......................................................................................... 472 Dostęp do pojedynczych znaków ........................................................................ 473 Wyszukiwanie fragmentów tekstu ...................................................................... 474 Pobieranie wycinka tekstu ................................................................................... 476 Strumienie standardowe ............................................................................................... 476 Podstawowe klasy strumieni ............................................................................... 476 Strumienie standardowe ...................................................................................... 477 Operatory strumieniowe ...................................................................................... 478 Wyprowadzanie informacji ................................................................................. 478 Formatowanie wyprowadzanych danych ............................................................ 479 Wprowadzanie danych ........................................................................................ 480 Wykrywanie końca danych wejściowych ............................................................ 482 Strumienie zbiorów pamięci masowej .......................................................................... 482 Tryby otwarcia zbioru ......................................................................................... 483 Otwieranie zbioru ................................................................................................ 484 Zamykanie zbioru ................................................................................................ 485 Serializacja i deserializacja danych ..................................................................... 485 Zbiory binarne ..................................................................................................... 485 Strumienie wirtualne .................................................................................................... 490 Wirtualny strumień wejściowy ............................................................................ 490 Wirtualny strumień wyjściowy ........................................................................... 491 Podsumowanie .............................................................................................................. 492

Rozdział 17. Obsługa sytuacji wyjątkowych ...................................................... 495 Sytuacje wyjątkowe ...................................................................................................... 496 Opis obiektowy ................................................................................................... 497 Terminologia ....................................................................................................... 498 Wydajność ........................................................................................................... 498 Standardowe klasy sytuacji wyjątkowych ........................................................... 499 Obsługa sytuacji wyjątkowych ..................................................................................... 501 Blok chroniony .................................................................................................... 501 Zgłaszanie sytuacji wyjątkowej ........................................................................... 502 Blok obsługi sytuacji wyjątkowych ..................................................................... 504 Zmienne w bloku chronionym ............................................................................ 509 Specyfikacje sytuacji wyjątkowych .............................................................................. 509 Specyfikacja sytuacji wyjątkowej ....................................................................... 510 Specyfikacja pusta ............................................................................................... 511 Specyfikacja noexcept ......................................................................................... 512 Podsumowanie .............................................................................................................. 513

10

Spis treści

Rozdział 18. Iteratory ...................................................................................... 515

Teoria iteratorów .......................................................................................................... 516 Postać iteratora .................................................................................................... 516 Rodzaje iteratorów .............................................................................................. 516 Operacje wykonywane na iteratorze .................................................................... 518 Iteratory standardowe ................................................................................................... 519 Szablon std::iterator ............................................................................................ 520 Zwyczajowe nazwy klas iteratorów .................................................................... 520 Pobieranie iteratorów .......................................................................................... 521 Przemieszczanie iteratora .................................................................................... 522 Pomiar odległości iteratorów ............................................................................... 523 Własne klasy iteratorów ............................................................................................... 524 Przykładowa klasa kontenera .............................................................................. 525 Klasa iteratora ..................................................................................................... 526 Pola i ich inicjalizacja ......................................................................................... 526 Przeciążenie operatorów ..................................................................................... 527 Metody tworzące iteratory ................................................................................... 530 Całość klasy kontenera i iteratora ........................................................................ 530 Iterowanie kolekcji z wykorzystaniem iteratorów ............................................... 531 Przemieszczenie i odległość ................................................................................ 532 Program przykładowy ......................................................................................... 532 Podsumowanie .............................................................................................................. 534

Rozdział 19. Biblioteka standardowa STL ......................................................... 537

Pojęcia podstawowe ..................................................................................................... 538 Alokatory ............................................................................................................ 538 Pary ..................................................................................................................... 539 Kontenery ..................................................................................................................... 540 Podstawowe struktury przechowywania danych ................................................. 540 Podstawowe operacje .......................................................................................... 541 Tablica dynamiczna ............................................................................................. 543 Tablica segmentowa ............................................................................................ 548 Lista ..................................................................................................................... 550 Adaptery ....................................................................................................................... 553 Stos ...................................................................................................................... 553 Kolejka ................................................................................................................ 555 Tablica asocjacyjna ............................................................................................. 558 Zbiory elementów ............................................................................................... 562 Algorytmy .................................................................................................................... 564 Zamiana wartości ................................................................................................ 565 Zliczanie elementów ........................................................................................... 565 Określanie wartości skrajnych ............................................................................. 565 Porównywanie elementów .................................................................................. 566 Kopiowanie elementów ....................................................................................... 567 Zapełnianie podzbioru elementów ...................................................................... 568 Porządkowanie elementów .................................................................................. 568 Podsumowanie .............................................................................................................. 569

Rozdział 20. Programowanie wielomodułowe .................................................... 571 Przestrzenie nazw ......................................................................................................... 571 Klasy ............................................................................................................................. 572 Szablony ....................................................................................................................... 572 Podsumowanie .............................................................................................................. 573

Dodatek

Tabela kodów znaków ASCII ......................................................... 575 Skorowidz .................................................................................... 581

Wstęp Programy komputerowe można pisać w jednym z wielu opracowanych języków programowania wysokiego poziomu. Niektóre z języków dawno już odeszły w zapomnienie i używane są tylko w szczególnych przypadkach — przykładem może być Fortran, stosowany jedynie do modernizacji istniejącego oprogramowania lub tworzenia pakietów wymagających korzystania ze specyficznych, wysoce specjalizowanych, doskonale opracowanych i przetestowanych bibliotek podprogramów napisanych w tym języku. Inne charakteryzują się daleko posuniętą specjalizacją, jak np. LOGO, nakierowane są przede wszystkim na wektorową grafikę żółwia i operują na strukturach danych zwanych listami, ale nie nadają się zbytnio do tworzenia poważniejszego oprogramowania i przeznaczone są do roli języka dydaktycznego (choć i ten bastion będą musiały opuścić wraz z upadkiem idei nauczania programowania mikrokomputerów w każdej szkole). Pozostałe — np.: Java, BASIC czy Pascal — najczęściej pretendują do miana uniwersalnych języków programowania, umożliwiających rozwiązanie każdego lub prawie każdego problemu (chociaż nie zawsze w sposób satysfakcjonujący czy optymalny). Istnieją jednak dwa języki programowania, które zostały szeroko zaakceptowane jako uniwersalne i wystarczająco potężne, by można było za ich pomocą tworzyć zarówno systemy operacyjne, jak i oprogramowanie użytkowe różnego typu. Dzięki względnie prostej budowie nie są trudne do przyswojenia i opanowania, a rezygnacja z wprowadzania wysokiego poziomu abstrakcji (zatem silny związek z budową komputera, na którym działa tworzony program) umożliwia korzystanie z nich w czasie pisania oprogramowania systemowego: systemów operacyjnych czy sterowników urządzeń. Te języki to C oraz C++.

Język C Język C szczególnie upodobali sobie twórcy oprogramowania systemowego. Opracowany i zaimplementowany w latach 1971 – 1972 przez Dennisa Ritchiego w czasie tworzenia systemu operacyjnego UNIX, od początku miał łączyć prostotę języka wysokiego poziomu (niewielka liczba słów kluczowych, naturalny zapis operacji arytmetycznych, automatyczne zarządzanie zmiennymi, proste tworzenie i wywoływanie podprogramów)

12

Wstęp

z mocą asemblera. Dzięki temu umożliwiał swobodny dostęp do wszystkich możliwości mikroprocesora i mikrokomputera, pozwalając na operowanie na danych o wielkościach od jednego bitu aż do całej dostępnej pamięci operacyjnej i odwoływanie się do urządzeń wejścia-wyjścia. Dodatkowo kod systemu operacyjnego napisany w języku C był, w przeciwieństwie do asemblera, przenośny — aby stworzony system uruchomić na komputerze o innej architekturze lub innym procesorze, wystarczyło napisać kompilator języka C przeznaczony dla danego komputera, wprowadzić nowe wersje podstawowych modułów jądra systemu operacyjnego (tak, by było ono w stanie zarządzać inaczej zbudowanym komputerem) i skompilować całość. Programy narzędziowe korzystające z usług systemu operacyjnego nie wymagały żadnych zmian, by działać na zupełnie innej maszynie. Chociaż początkowe wersje systemów operacyjnych Microsoft Windows oraz Microsoft/IBM OS/2 były pisane bezpośrednio w języku asemblera (miały działać z najwyższą wydajnością na powolnych wtedy jeszcze komputerach osobistych klasy IBM PC, a nikt nie przejmował się ich przenośnością), z czasem i do tych dwóch światów dotarł język C. System OS/2 od wersji 2.0 był uzupełniany wstawkami napisanymi w języku C, by uprościć zarządzanie kodem i przyspieszyć prace programistyczne. Z kolei firma Microsoft od podstaw tworzyła w języku C rodzinę systemów Windows NT, od samego początku pomyślanych jako w pełni przenośne (pierwsze wersje Windows NT dostępne były dla procesorów Intel IA-32, DEC Alpha, MIPS oraz PowerPC). Wcześniejsze wersje systemu Windows również były już rozbudowywane za pomocą języka C, który od dawna stanowił też podstawową platformę przygotowywania programów użytkowych dla tego środowiska graficznego. Jądro systemu Linux także napisane jest w języku C, a jego twórcy mocno sprzeciwiają się jakimkolwiek próbom wprowadzania fragmentów kodu C++. Słusznie argumentują przy tym, że w przypadku oprogramowania niskiego poziomu, takiego jak system operacyjny — współpracującego bezpośrednio ze sprzętem, wymagającego prostej składni, efektywnych kompilatorów i szerokiej przenośności — język C jest zdecydowanie bardziej efektywny niż C++. Przeciętny programista, który chce tworzyć oprogramowanie automatyzujące skomplikowane lub czasochłonne zadania, nie potrzebuje aż tak wyrafinowanej kontroli nad przebiegiem działania programu. Istotniejsze w takim przypadku stają się: prostota implementacji nawet rozbudowanych algorytmów i struktur danych, przeniesienie jak największej części pracy na kompilator języka i jego biblioteki oraz ograniczenie liczby błędów wprowadzanych w trakcie rozwoju programu. Język C, choć prosty i bardzo skuteczny, musi zostać zastąpiony czymś bardziej odpowiednim — np. językiem C++.

Język C++ C++ (nazwa wywodzi się bezpośrednio ze składni języka C, w którym C++ oznacza „C zwiększone o jeden”) opracowany został w latach 1983 – 1985 przez Bjarne Stroustrupa. Początkowo C++ nie był oddzielnym językiem programowania — kod napisany w C++ należało przetworzyć tzw. preprocesorem C++, który zamieniał struktury C++

Wstęp

13

na odpowiednie konstrukcje składniowe języka C. Wygenerowany w ten sposób kod źródłowy C nie był co prawda zbyt czytelny, ale był poprawny i można było skompilować go standardowym kompilatorem języka C. Wraz ze wzrostem liczby mechanizmów wprowadzanych do języka C++, a nieobecnych w C, niezbędne stało się wyodrębnienie osobnej klasy kompilatorów języka C++ — i tak pozostało do dzisiaj. Język C++ rozszerza język C o mechanizmy klasyfikowane ogólnie jako mechanizmy programowania obiektowego. Programowanie obiektowe stanowi kolejny krok ku zwiększeniu abstrakcji języka programowania. Proceduralne języki programowania pozwalają tworzyć podprogramy dokonujące operacji na zmiennych, jednak zmienne są tylko reprezentacją wydzielonego obszaru pamięci operacyjnej i poza nazwą oraz typem1 nie charakteryzują się niczym specjalnym. Programista może użyć jako zmiennej dowolnego obszaru pamięci, co w większości przypadków nie powoduje żadnych problemów. Język C++ wprowadza pojęcie obiektu, czyli zmiennej, która zapisana jest w konkretnej postaci uwzględniającej istnienie tzw. metod (podprogramów operujących wyłącznie na konkretnym obiekcie) oraz pól (danych zgrupowanych razem w ramach jednego obiektu). Metoda jest niedostępna w oderwaniu od samego obiektu, a opcjonalny mechanizm identyfikowania typów zmiennych pozwala już w trakcie działania programu określać, z jakiego typu zmiennymi ma się do czynienia. Pozostałe istotne pojęcia wprowadzone wraz z językiem C++ to m.in.:  dziedziczenie klas obiektów — tworzenie nowych klas obiektów

rozszerzających możliwości już istniejących klas za pomocą dodatkowych zmiennych lub metod;  polimorfizm — zastępowanie metod klasy nadrzędnej (z której dziedziczy

klasa pochodna) nową metodą, która wywoływana jest zawsze zamiast oryginalnej;  przeciążanie podprogramów i metod — deklarowanie wielu podprogramów

i metod o tych samych nazwach, operujących jednak na różnym zestawie danych dowolnego typu; dzięki temu dwa podprogramy Uśrednij, z których jeden uśrednia tablicę trzech liczb rzeczywistych, a drugi pięciu liczb całkowitych, mogą mieć tę samą nazwę, a mimo to nie dojdzie do konfliktu;  przeciążanie operatorów — tworzenie podprogramów obsługujących operatory

arytmetyczne i logiczne; dzięki temu klasa obsługująca nowy typ danych może dopuszczać naturalny zapis operacji modyfikujących obiekty lub tworzących nowe obiekty na bazie już istniejących, np. klasa obiektu reprezentującego wektor zakotwiczony w początku układu współrzędnych o trzech współrzędnych końca x,y,z może przeciążyć operator sumy +, by prosty zapis Wektor1 + Wektor2 powodował utworzenie trzeciego wektora, będącego sumą dwóch podanych w równaniu;  obsługa sytuacji wyjątkowych — reagowanie na operacje zabronione, awaryjne

lub bezsensowne (dzielenie przez zero, błąd operacji wejścia-wyjścia, brak

1

Typ zmiennej jest określany tylko na etapie tworzenia programu w postaci źródłowej.

14

Wstęp

pamięci operacyjnej) i wycofywanie się z realizowanych działań w sposób kontrolowany, często umożliwiający nieprzerwane działanie programu mimo poważnych zakłóceń jego funkcjonowania;  dynamiczne identyfikowanie typów obiektów — dzięki temu podprogram

oczekujący jako parametru obiektu danego typu może bez problemu określić, czy obiekt przekazany jako parametr rzeczywiście jest wymaganego typu, czy też programista przekazuje obiekt klasy pochodnej;  szablony — mechanizm umożliwiający tworzenie wzorców podprogramów

i klas obiektów, które automatycznie są przekształcane w sposób pozwalający na wykorzystywanie ich w powiązaniu z dowolnym typem danych, bez żmudnego kopiowania fragmentów kodu, nadawania mu nowych nazw i wprowadzania drobnych poprawek (np. po to, by klasa obiektu tworzącego listę wielu elementów mogła też obsługiwać listę liczb rzeczywistych, a nie tylko całkowitych);  przestrzenie nazw — przedrostki umożliwiające wprowadzenie logicznego

podziału kodu programu na moduły i stosowanie w każdym z tych modułów elementów (funkcji, zmiennych) o tych samych nazwach bez ryzyka konfliktu;  strumienie — mechanizm przesyłania danych, upraszczający zapis kodu

komunikującego się z urządzeniami peryferyjnymi (ekran komputera, klawiatura, drukarka, zbiory w pamięci masowej).

Zawartość książki Ta książka stanowi kompletny kurs języków C i C++. Podzielona jest na trzy części:  Część I opisuje pobranie, instalowanie i podstawy korzystania ze środowiska

programistycznego Microsoft Visual Studio 2012 Express. Ponieważ konkretne funkcje środowiska najlepiej jest omawiać na prawdziwych przykładach, dalszy opis możliwości tego środowiska programistycznego rozproszony jest na dwie kolejne części książki. Należy podkreślić jednak, że ta książka nie stanowi kompletnego kursu korzystania z Microsoft Visual Studio 2012 Express i omawia tylko te funkcje, które są niezbędne do zrealizowania przykładów i ćwiczeń przedstawionych w tekście.  Część II stanowi kurs programowania w języku C. Język C++ to nadzbiór

języka C, zatem dokładne opanowanie języka C upraszcza zrozumienie mechanizmów języka C++ przenoszących programowanie na wyższy poziom abstrakcji.  Część III przedstawia podstawowe techniki programowania w języku C++.

Jest to kurs podstawowy, nieuwzględniający (z powodu ograniczeń wynikających z objętości książki) konstrukcji programowych jeszcze wyższego poziomu abstrakcji, które można zbudować za pomocą mechanizmów udostępnianych przez język C++. Ten kurs pozwala jednak tworzyć własne, obiektowo

Wstęp

15

zorientowane programy w języku C++ oraz ułatwia przyswojenie treści książek opisujących konkretne konstrukcje programowe, których autorzy założyli znajomość języka C++. Ze względu na kompleksowość kursu — obejmuje on podstawy, od instalowania środowiska programistycznego przez najprostsze, banalne wręcz programy demonstrujące możliwość zmuszenia maszyny do wykonywania poleceń programisty aż po pełen opis składni języków C i C++ — książka przeznaczona jest przede wszystkim dla osób początkujących, które o programowaniu (szczególnie w językach C i C++) nie wiedzą nic lub wiedzą niewiele. Takie osoby po przeczytaniu tej książki powinny być w stanie samodzielnie napisać własny program i dzięki umiejętności programowania pokonywać trudności, które dotychczas wydawały się nie do pokonania ze względu na ogrom potrzebnej pracy i brak specjalizowanego oprogramowania. Komputer to przecież przede wszystkim niezwykle uniwersalna maszyna, która powinna wykonywać polecenia użytkownika i ułatwiać mu pracę, nie zaś stanowić źródło frustracji i powodować stratę wielu godzin spędzanych na poszukiwaniu programu oferującego potrzebne funkcje. Tematyka książki nie obejmuje tworzenia graficznych interfejsów użytkownika ani tworzenia grafiki. Standard języków C i C++ nie obejmuje grafiki, przez co na każdej platformie sprzętowej i programowej grafikę i interfejsy użytkownika tworzy się całkowicie odmiennie. Nie ma również uzasadnienia zwiększanie stopnia skomplikowania kursu przez wprowadzanie dziesiątek lub setek podprogramów i klas obsługujących grafikę i graficzny interfejs użytkownika w sytuacji, gdy problemem jest korzystanie z podstawowych elementów języka. Jako zasadę należy przyjąć zatem dokładne opanowanie sposobu tworzenia oprogramowania korzystającego z tzw. tekstowej konsoli systemu (czyli ekranu pracującego w trybie znakowym); dopiero po pokonaniu tego etapu warto sięgnąć po książki opisujące tworzenie aplikacji graficznych.

Trochę teorii Kod maszynowy Program, aby mógł być zrealizowany przez mikroprocesor komputera, musi zostać przygotowany w specjalnej, zrozumiałej dla danego mikroukładu postaci zwanej kodem maszynowym. Kod maszynowy to ciąg liczb, z których każda oznacza jeden rozkaz mikroprocesora (każdemu rozkazowi przyporządkowana jest jedna liczba lub ciąg liczb) lub dane uzupełniające rozkaz. Pisanie programów bezpośrednio w kodzie maszynowym jest możliwe, lecz ekstremalnie niewygodne, uciążliwe i pracochłonne. Rozsądek nakazuje tworzyć programy w języku bardziej zrozumiałym dla człowieka, a następnie tłumaczyć je automatycznie (za pomocą odpowiedniego programu komputerowego) do postaci kodu maszynowego. Takie programy tłumaczące nazywa się translatorami i w zależności od sposobu działania dzieli się na podgrupy:

16

Wstęp  programy asemblera,  interpretery języków wysokiego poziomu,  kompilatory języków wysokiego poziomu.

Asembler Asembler to język programowania, w którym poszczególnym rozkazom mikroprocesora przyporządkowano nie liczby, lecz nazwy (tzw. mnemoniki). Dodatkowo dozwolone jest wykorzystywanie nazw symbolicznych, automatycznie zamienianych na adresy pamięci wewnętrznej komputera lub liczby. W ten sposób mimo że program powstaje dalej w języku maszynowym, dodatkowa warstwa abstrakcji niezmiernie skraca czas tworzenia programu, zwiększa komfort programisty i zmniejsza stopę błędów. Program, który dokonuje tłumaczenia z języka asemblera do postaci kodu maszynowego, nosi nazwę programu asemblera. Program asemblera stanowi pewną specyficzną, bardzo uproszczoną formę kompilatora (omówionego w jednym z dalszych podpunktów). Więcej informacji o języku asemblera i programach asemblera znajdziesz pod adresem: http://www.grush.one.pl/article.php?id=asembler.

Interpreter Niektóre translatory dokonują tłumaczenia w czasie wykonywania programu, tłumacząc tylko aktualnie realizowany wiersz programu i zapominając o nim po skończonej pracy. Taki translator nazywa się interpreterem, gdyż na bieżąco interpretuje pojedyncze instrukcje lub ich sekwencje i wykonuje je, nie tworząc jednak kopii programu w języku maszynowym, która byłaby gotowa do natychmiastowego zrealizowania. Interpretacja programu ma tylko jedną zaletę: prostotę i szybkość uruchamiania. Interpreter może wykonywać nawet pojedyncze, podawane bezpośrednio z klawiatury przez użytkownika, wiersze programu, ułatwiając testowanie działania jakiejś instrukcji lub umożliwiając realizowanie prostych operacji arytmetycznych bez uruchamiania (lub pisania) specjalizowanego programu. Uruchomienie programu nie wymaga też żadnych dodatkowych kroków, gdyż interpreter natychmiast rozpoczyna tłumaczenie pierwszego wiersza i realizowanie zawartych w nim instrukcji. Zaletom tym przeciwstawiają się poważne wady. Po pierwsze, każda osoba chcąca uruchomić program musi posiadać na swoim komputerze interpreter języka, w którym ten program został napisany (najlepiej tę samą wersję interpretera, który został użyty do napisania programu, by uniknąć błędów). Po drugie, ponieważ wykonanie każdego wiersza programu wymaga jego przetłumaczenia, działanie interpretowanego programu może być nawet dziesięć lub dwadzieścia razy wolniejsze niż programu w postaci kodu maszynowego. Po trzecie, raz przetłumaczony wiersz programu jest od razu zapomi-

Wstęp

17

nany i powtórne jego wykonanie wymaga tłumaczenia po raz kolejny. Z tych powodów interpretery języków były wykorzystywane tylko w prostych mikrokomputerach domowych i obecnie nie grają dużej roli w rozwoju profesjonalnego oprogramowania.

Kompilator Przeciwieństwem interpreterów są kompilatory. Kompilator nie potrafi uruchamiać programu użytkownika: jego zadaniem jest tłumaczenie programów z postaci źródłowej na postać kodu maszynowego i zapisanie wyniku tłumaczenia w pamięci tak, by można było uruchomić program z poziomu samego systemu operacyjnego. Skompilowany program można do woli kopiować na różne komputery, nie wymaga bowiem obecności kompilatora i uruchomi się bez żadnych kłopotów. Program będzie też działał z najwyższą możliwą wydajnością, gdyż po kompilacji ma postać kodu maszynowego i mikroprocesor ma za zadanie jedynie odczytywać i realizować kolejne, przetłumaczone już instrukcje. Nawet jeżeli wiersz programu miał być wykonywany tysiąc razy, tłumaczenie przebiega tylko raz — podczas samej kompilacji. Kompilacja programów ma jednak swoje wady. Uruchomienie programu wymaga wcześniejszego jego skompilowania i nawet jeżeli używane przez programistę środowisko programistyczne (np. Microsoft Visual C++) potrafi automatycznie rozpocząć kompilację w momencie wydania polecenia uruchomienia programu, minie co najmniej kilka sekund, zanim program zostanie uruchomiony. Jeżeli odkryje się najmniejszy błąd w tworzonym programie — choćby nierówną kolumnę liczb wymagającą dodania lub usunięcia kilku znaków odstępu — trzeba skompilować cały program od początku. Kompilator nie jest też w stanie wykonać pojedynczych poleceń podanych bezpośrednio z klawiatury. Nawet jeżeli chce się tylko przetestować działanie jakiejś jednej instrukcji, trzeba stworzyć najkrótszy kompletny program o odpowiedniej strukturze i poddać go kompilacji. Interpreter, wykonując program wiersz po wierszu, może na bieżąco śledzić jego działanie, wykrywać błędy z dokładnością do jednej instrukcji i nawet wznawiać działanie programu od miejsca wystąpienia błędu (po jego usunięciu). W przypadku programu kompilowanego diagnozowanie błędów może być udręką, ponieważ kompilator traci kontrolę nad programem w momencie wygenerowania kodu maszynowego. Proste błędy polegające na naruszeniu zasad składni języka (błędy składniowe) czy wywoływaniu nieistniejącego podprogramu zostaną wychwycone i zgłoszone w czasie kompilacji, ale jeżeli tworzony program np. spróbuje dokonać operacji dzielenia przez zero, system operacyjny zgłosi tylko fakt wystąpienia problemu, nie dając żadnej konkretnej wskazówki, gdzie go szukać. Rozbudowane środowiska programistyczne zawierają systemy uruchomieniowe pozwalające śledzić działanie programu zapisanego w kodzie maszynowym i znacznie prościej lokalizować miejsca wystąpienia błędów w kodzie źródłowym. Mimo wszystkich wad, szybkość programów poddawanych kompilacji przeważyła i olbrzymia większość powstającego dzisiaj oprogramowania to produkty skompilowane, gotowe do natychmiastowego użycia z najwyższą wydajnością. Również programy powstające w językach C i C++ poddawane są kompilacji.

18

Wstęp

Kompilator JIT Możliwe jest połączenie cech interpretera i kompilatora. Translator łączący ich cechy, nazywany kompilatorem JIT (ang. just-in-time compiler), dokonuje tłumaczenia fragmentu tekstu źródłowego programu (najczęściej całego podprogramu) w momencie rozpoczęcia jego wykonania (stąd nazwa JIT) i zachowuje przetłumaczoną kopię aż do momentu zakończenia pracy programu. W ten sposób w pamięci obecne są fragmenty kodu maszynowego gotowe do powtórnego wykonania z pominięciem procesu tłumaczenia2. Najpopularniejsze obecnie języki obsługiwane przez kompilatory JIT to Java oraz C#. Wydajność tak wykonywanych programów zbliża się do wydajności programów klasycznie skompilowanych lub chwilami nawet ją przekracza, jednak za cenę zwiększonego zużycia pamięci operacyjnej: oprócz skompilowanych fragmentów kodu trzeba przechowywać w pamięci tekst źródłowy programu oraz sam kompilator JIT. Kolejną wadą odziedziczoną po interpreterach jest konieczność zainstalowania kompilatora JIT na każdym komputerze, który ma korzystać z tak uruchamianego programu (sam kompilator bez środowiska programistycznego często nazywa się maszyną wirtualną).

Problem jajka i kury Skoro aby uzyskać program w kodzie maszynowym, trzeba skorzystać z translatora języka wysokiego poziomu, w jaki sposób tworzy się te translatory? Oczywiście, translator to zwykły program. Można go napisać w dowolnym języku, skompilować i używać. By jednak dokonać kompilacji, trzeba posiadać jakiś kompilator (czyli translator). Właśnie natknęliśmy się na klasyczny problem: co było pierwsze — jajko czy kura? Nowe wersje translatorów najczęściej tworzy się za pomocą starszych wersji. Gotowy produkt, zdolny do tłumaczenia programów na kod maszynowy, może też skompilować samego siebie np. po to, by uzyskać wyższą jakość kodu (starsza wersja kompilatora wygenerowała poprawny kod, ale być może nie tak szybki, jaki potrafi stworzyć nowa). Nie rozwiązuje to jednak problemu: kiedyś musiała powstać pierwsza wersja translatora. Nie było innego wyjścia: pierwszą wersję translatora programiści musieli napisać bezpośrednio w kodzie maszynowym, ręcznie tłumacząc swój program na liczby i wprowadzając je do pamięci komputera. Dopiero w tym momencie można było rozpocząć rozwijanie większych, bardziej skomplikowanych i doskonalszych translatorów.

2

Stanowi to oczywistą zaletę, jeżeli dany fragment programu jest wykonywany więcej niż raz. Jednak w przypadku, gdy mamy do czynienia z fragmentem wykonywanym tylko jednokrotnie, kompilacja JIT może być nieefektywna, gdyż niepotrzebny już fragment programu będzie nie tylko obecny w pamięci, ale też obecny dwukrotnie: raz w formie źródłowej (lub wstępnie przetworzonej), a raz w formie skompilowanego kodu maszynowego.

Część I

Środowisko pracy By tworzyć oprogramowanie w językach C lub C++, trzeba dysponować następującymi narzędziami:  edytorem, za pomocą którego wprowadza się tekst źródłowy programu,

przegląda go, modyfikuje i opatruje komentarzem pozwalającym zwiększyć czytelność tekstu;  biblioteką podstawową zawierającą gotowe podprogramy realizujące najczęściej

wykonywane operacje (np. wyznaczanie pierwiastka kwadratowego liczby, wyznaczanie wartości funkcji trygonometrycznych dowolnych argumentów, badanie liczby znaków w napisie) i w ten sposób uwalniające programistę z konieczności samodzielnego tworzenia tego typu podprogramów;  kompilatorem, którego zadaniem jest przetwarzanie składowych plików

z kodem źródłowym programu na postać zrozumiałą dla mikroprocesora (tzw. kod maszynowy), bezpośrednio przez niego później wykonywaną;  konsolidatorem, który łączy osobne skomplikowane moduły programu w jeden

gotowy plik wykonywalny, kierowany przez użytkownika do realizacji (wykonania);  modułem uruchomieniowym pozwalającym nadzorować (w pewnym sensie:

śledzić) działanie własnego programu, wstrzymywać jego realizację na czas analizy stanu, w jakim się znalazł, podglądać zawartość pamięci należącej do programu oraz wprowadzać poprawki. Teoretycznie każde z tych narzędzi może zostać pozyskane niezależnie i pochodzić od innego dostawcy. Ze względów praktycznych wygodnie jest jednak, jeżeli wszystkie one podlegają jednoczesnej instalacji i mogą współpracować ze sobą. W przypadku systemu operacyjnego Microsoft Windows jednym z najbardziej znanych środowisk programistycznych łączących wymienione powyżej narzędzia jest Microsoft Visual Studio. Jest to profesjonalny system tworzenia oprogramowania, przeznaczony zarówno dla początkujących programistów, jak i dla firm zajmujących się tworzeniem i testowaniem oprogramowania. Dostępne wydania pakietu różnią się zakresem

20

Część I  Środowisko pracy

oferowanej funkcjonalności oraz ceną, jednak dla użytkowników indywidualnych — a tym bardziej dla tych, którzy dopiero uczą się programowania — najciekawsze będzie darmowe wydanie Express. Oczywiście, nie ma róży bez kolców. O ile komercyjne wersje Microsoft Visual Studio pozwalają tworzyć oprogramowanie w wielu różnych językach programowania, a nawet wykorzystywać kilka z nich w ramach jednego programu, o tyle wydanie Express pozwala wybrać tylko jeden. Ponadto programy napisane za jego pomocą nie mogą być sprzedawane.

Rozdział 1.  Środowisko programistyczne Microsoft Visual Studio Express 2012

21

Rozdział 1.

Środowisko programistyczne Microsoft Visual Studio Express 2012 Pakiet Microsoft Visual Studio Express, który będzie służył do wprowadzania i uruchamiania programów przedstawionych w dalszych rozdziałach książki, można pobrać za darmo ze strony WWW firmy Microsoft. Aby korzystać z pakietu przez więcej niż 30 dni, należy zarejestrować się jako użytkownik, co nie wiąże się na szczęście z żadnymi opłatami.

Pobranie programu instalacyjnego Aby pobrać wersję instalacyjną pakietu (rysunek 1.1), należy otworzyć okno przeglądarki WWW i przejść do strony o następującym adresie: http://www.microsoft.com/ visualstudio/plk/products/visual-studio-express-products. Po kliknięciu pozycji Produkty Express 2012 rozwinie się lista odmian środowiska, obejmująca m.in. wersje pozwalające tworzyć oprogramowanie dla nowego dotykowego środowiska graficznego stosowanego w Windows Phone, Windows RT oraz Windows 8. Do tworzenia programów uruchamianych na standardowym pulpicie systemów Windows w dowolnej wersji służy wydanie Express dla Windows Desktop. Kliknięcie tej pozycji listy spowoduje przejście na stronę oferującą pobranie wersji instalacyjnej — w tym celu należy kliknąć odnośnik Pobierz wyświetlany na obrazie po prawej stronie okna (rysunek 1.2).

22

Część I  Środowisko pracy

Rysunek 1.1. Strona główna pakietu Microsoft Visual Studio Express 2012

Rysunek 1.2. Strona WWW oferująca możliwość pobrania pakietu Microsoft Visual Studio Express 2012 dla Windows Desktop

Rozdział 1.  Środowisko programistyczne Microsoft Visual Studio Express 2012

23

Dotychczasowe wersje środowisk programistycznych firmy Microsoft były dostępne wyłącznie w kilku najważniejszych wersjach językowych; w Polsce najczęściej stosowano wersję anglojęzyczną1. Od wersji 2012 środowisko Microsoft Visual Studio Express może zostać spolonizowane za pomocą pakietu językowego (rysunek 1.3). Najpierw jednak należy zainstalować podstawową wersję językową, np. angielską.

Rysunek 1.3. Centrum pobierania plików instalacyjnych Microsoft Visual Studio Express 2012 i pakietów językowych

Instalowanie należy rozpocząć od kliknięcia odnośnika Zainstaluj teraz. Przeglądarka WWW rozpocznie pobieranie względnie niewielkiego pliku instalacyjnego o nazwie wdexpress_full.exe, zadając przy tym pytanie o działanie, które ma zostać podjęte wobec pliku po jego pobraniu (rysunek 1.4). Aby uruchomić program instalacyjny i kontynuować instalowanie bezpośrednio po pobraniu pliku, należy kliknąć przycisk Uruchom.

Rysunek 1.4. Pytanie o chęć pobrania instalatora pakietu Microsoft Visual Studio Express

1

Ponieważ programiści muszą biegle posługiwać się językiem angielskim, w którym powstają słowa kluczowe języków programowania i w którym publikowana jest dokumentacja wszystkich narzędzi i interfejsów, nie było to nigdy problemem. Udostępnienie wersji językowych środowiska — w tym polskiej — jest raczej ukłonem w stronę początkujących niż funkcją cenną dla profesjonalistów.

Część I  Środowisko pracy

24

Instalacja Po pomyślnie zakończonym pobraniu pliku wdexpress_full.exe program instalacyjny zostanie uruchomiony. Przez chwilę na ekranie będzie wyświetlane logo pakietu Microsoft Visual Studio (rysunek 1.5), które po chwili zostanie zastąpione planszą programu instalacyjnego (rysunek 1.6). Można na niej dokonać następujących zmian w parametrach instalacji:  Wybrać partycję docelową oraz folder docelowy instalacji. Domyślnie program

instalowany jest w folderze Microsoft Visual Studio 11.0 na partycji zawierającej system operacyjny oraz folder Program Files2. Aby dokonać zmiany, należy kliknąć przycisk z trzema kropkami znajdujący się obok pola ścieżki dostępu i w polu dialogowym wskazać właściwy folder3.  Zaakceptować warunki umowy licencyjnej. Wykonanie tego kroku jest

warunkiem koniecznym, by możliwe było przejście do kolejnych etapów instalacji4. Aby zaakceptować umowę, należy umieścić znacznik w polu I agree to the License terms and conditions.  Zgodzić się na przekazywanie anonimowych danych o użytkowaniu pakietu

w celu określania obszarów, w których należy dokonać zmian w pakiecie, by podnieść jego funkcjonalność oraz łatwość użycia. Jeżeli chcesz wyrazić zgodę na uczestnictwo w programie, zaznacz pole Join the Customer Experience Improvement Program to help improve the quality, reliability and performance of Visual Studio5. Rysunek 1.5. Logo sygnalizujące rozpoczęcie instalowania pakietu

Aby rozpocząć instalowanie, kliknij przycisk Install. Ponieważ instalowanie oprogramowania wymaga uprawnień administratora systemu, na ekranie pojawi się w tym momencie ciemna plansza systemu UAC6, za pośrednictwem której system operacyjny 2 3

4

5

6

Zazwyczaj jest to partycja C:. Partycja, na której instaluje się pakiet Microsoft Visual Studio, powinna zawierać co najmniej 5 GB wolnej przestrzeni. Dodatkowo, niezależnie od wskazanego folderu i partycji, użyta zostanie pewna ilość miejsca na partycji zawierającej pliki systemu operacyjnego. Jeżeli pole I agree to the License terms and conditions nie zostanie zaznaczone, w polu dialogowym instalatora w ogóle nie pojawi się przycisk Install, umożliwiający rozpoczęcie instalowania pakietu. Więcej informacji o tym programie można znaleźć pod adresem: http://www.microsoft.com/products/ ceip/PL-PL/default.mspx. Ang. User Account Control — mechanizm wprowadzony w systemie Microsoft Windows Vista, wymagający dodatkowego potwierdzenia operacji wpływających na działanie całego systemu lub mogących zaszkodzić jego bezpieczeństwu i stabilności. Umożliwia pracę na co dzień z uprawnieniami zwykłego użytkownika systemu, w razie potrzeby — po podaniu hasła — podnosi chwilowo uprawnienia.

Rozdział 1.  Środowisko programistyczne Microsoft Visual Studio Express 2012

25

Rysunek 1.6. Główna plansza programu instalacyjnego

domaga się zgody na zainstalowanie programu z podwyższonymi uprawnieniami (rysunek 1.7). Aby udzielić zgody, należy kliknąć przycisk Tak. Spowoduje to rozpoczęcie instalacji, o której postępie będzie informowała para pasków postępu (rysunek 1.8): pierwszy o stopniu zaawansowania pobierania plików instalacyjnych na dysk twardy komputera7, drugi — o stopniu zaawansowania instalacji pakietu w systemie. Rysunek 1.7. Potwierdzenie praw administratora

7

Użytkownik pobiera wyłącznie niewielkiej objętości program instalacyjny. Właściwe pliki pakietu Microsoft Visual Studio Express są pobierane na bieżąco w czasie instalacji.

Część I  Środowisko pracy

26 Rysunek 1.8. Trwa instalowanie pakietu

Pierwsze uruchomienie Gdy instalacja pakietu zostanie pomyślnie zakończona, plansza programu instalacyjnego przybierze wygląd jak na rysunku 1.9. U dołu planszy pojawi się przycisk Launch, umożliwiający uruchomienie i wstępne skonfigurowanie środowiska programistycznego. Po jego kliknięciu program instalacyjny poprosi o wprowadzenie numeru seryjnego, uzyskiwanego w ramach internetowej rejestracji użytkownika pakietu (rysunek 1.10). Bez rejestracji możliwe jest testowanie pakietu przez 30 dni, można zatem pominąć rejestrację zaraz po zainstalowaniu pakietu, by zapoznać się z jego działaniem przed udzieleniem zgody na warunki umowy licencyjnej. Aby zrezygnować z rejestracji, należy kliknąć przycisk , znajdujący się w prawym górnym rogu okna. Proces internetowej rejestracji użytkownika pakietu oraz wprowadzania numeru seryjnego jest opisany w dalszej części tego rozdziału.

Podczas pierwszego uruchamiania środowisko Microsoft Visual Studio Express musi stworzyć zestaw początkowych ustawień oraz zapisać go w rejestrze systemu i własnych plikach konfiguracyjnych, przechowywanych w profilu użytkownika (rysunek 1.11). Czas trwania tej operacji zależy od wydajności komputera, na którym uruchamia się program; zazwyczaj trwa ona nie dłużej niż kilka minut.

Rozdział 1.  Środowisko programistyczne Microsoft Visual Studio Express 2012 Rysunek 1.9. Instalacja zakończona — można przystąpić do pierwszego uruchomienia środowiska programistycznego

Rysunek 1.10. Pole dialogowe z prośbą o wprowadzenie numeru seryjnego

27

Część I  Środowisko pracy

28 Rysunek 1.11. Trwa wstępna konfiguracja środowiska pracy

Gdy wstępna konfiguracja się zakończy, na ekranie pojawi się główne okno środowiska programistycznego Microsoft Visual Studio Express (rysunek 1.12). To właśnie w tym oknie będą edytowane pliki z tekstem źródłowym programów, analizowane błędy, śledzona praca programu oraz wyświetlane wszystkie pola pomocnicze i palety narzędzi. Na razie jednak wszystkie elementy okna są wyświetlane w języku angielskim. By spolonizować pakiet, należy zamknąć okno i przystąpić do pobrania i zainstalowania nakładki polonizującej.

Rysunek 1.12. Główne okno środowiska programistycznego Microsoft Visual Studio Express

Instalacja pakietu polonizującego Polonizację pakietu należy rozpocząć od tej samej strony WWW, z której pobrany został program instalacyjny (rysunek 1.3). W panelu po prawej stronie okna przeglądarki należy z rozwijanej listy wybrać język polski, a następnie kliknąć odnośnik Pobierz teraz znaj-

Rozdział 1.  Środowisko programistyczne Microsoft Visual Studio Express 2012

29

dujący się w polu zatytułowanym Pakiet językowy programu Visual Studio Express 2012 dla Windows Desktop — polski. Tak jak poprzednio: należy nakazać uruchomienie pliku bezpośrednio po jego pobraniu z Internetu oraz poczekać na pomyślne zakończenie procesu pobierania. Od strony graficznej i użytkowej program instalacyjny pakietu polonizującego jest identyczny z programem instalacyjnym środowiska Microsoft Visual Studio Express (rysunek 1.13). Aby rozpocząć instalowanie, należy zaznaczyć pole Zgadzam się na Warunki licencji; opcjonalnie można również zgodzić się na przystąpienie do programu poprawy jakości obsługi klienta, zaznaczając odpowiednie pole wyboru8. Przycisk Zainstaluj, umożliwiający rozpoczęcie instalacji, pojawia się — jak poprzednio — dopiero w momencie wyrażenia zgody na warunki umowy licencyjnej. Rysunek 1.13. Plansza początkowa programu instalacyjnego pakietu językowego polonizującego środowisko Microsoft Visual Studio Express

Podobnie jak w przypadku instalacji samego pakietu jego polonizacja wymaga działania oprogramowania z podwyższonymi uprawnieniami. Zgody na to będzie się dopominał system operacyjny za pośrednictwem ciemnej planszy systemu UAC (rysunek 1.14). Aby kontynuować instalację, należy kliknąć przycisk Tak. Pobierany początkowo program polonizujący pobiera wszystkie potrzebne pliki z Internetu dopiero w momencie rozpoczynania polonizacji. O postępie procesu pobierania i instalowania plików informują — jak poprzednio — dwa odrębne paski postępu (rysunek 1.15). Gdy instalacja się zakończy, na ekranie zostanie wyświetlona stosowna

8

Szczegóły dotyczące tego programu zostały przedstawione we wcześniejszej części rozdziału.

Część I  Środowisko pracy

30 Rysunek 1.14. Potwierdzenie praw administratora

Rysunek 1.15. Trwa instalowanie pakietu polonizującego

informacja (rysunek 1.16) wraz z przyciskiem Zamknij, który kończy sam proces, lecz nie decyduje jeszcze o wyborze języka polskiego jako podstawowego dla interfejsu użytkownika środowiska programistycznego.

Uruchamianie Aby uruchomić środowisko programistyczne Microsoft Visual Studio pod kontrolą systemów operacyjnych Microsoft Windows Vista oraz Windows 7, należy otworzyć panel Start, rozwinąć kolejno pozycje Wszystkie programy oraz Microsoft Visual Studio 2012 Express, a następnie kliknąć ikonę VS Express for Desktop. W efekcie — po chwili oczekiwania — na ekranie pojawi się okno główne programu (rysunek 1.12).

Rozdział 1.  Środowisko programistyczne Microsoft Visual Studio Express 2012

31

Rysunek 1.16. Instalacja pakietu polonizującego została zakończona; nie oznacza to jednak jeszcze polonizacji pakietu

Jeżeli środowisko będzie uruchamiane dostatecznie często, jego ikona zostanie przez system umieszczona wśród najczęściej uruchamianych programów, bezpośrednio w panelu Start. W systemie Windows 7 można dodatkowo uprościć sobie uruchamianie środowiska, klikając ikonę prawym przyciskiem myszy i wybierając z menu kontekstowego pozycję Przypnij do paska zadań (rysunek 1.17). Ikona uruchamiająca środowisko będzie od tego momentu istniała tylko na pasku zadań niezależnie od tego, czy program jest uruchomiony, czy nie. Rysunek 1.17. Przypinanie ikony do paska zadań systemu Windows 7

W systemie Windows 8 płytka uruchamiająca środowisko programistyczne jest umieszczana na ekranie startowym systemu. Jej położenie może być dowolnie zmienione przez użytkownika metodą przeciągnij-i-upuść.

Część I  Środowisko pracy

32

Aby zrezygnować z przypięcia ikony programu do paska zadań, należy kliknąć ją prawym przyciskiem myszy i z menu kontekstowego wybrać pozycję Odepnij ten program od paska zadań.

Wybór języka interfejsu użytkownika Samo zainstalowanie pakietu językowego nie oznacza automatycznej zmiany wersji językowej interfejsu użytkownika środowiska. Aby wymusić zmianę, należy otworzyć pole dialogowe opcji środowiska (rysunek 1.18), wybierając pozycję Options z menu Tools, rozwinąć w drzewie kategorii po lewej stronie okna pozycję Environment, wskazać kliknięciem kategorię podrzędną International Settings i z rozwijanej listy Language wybrać język polski.

Rysunek 1.18. Wybór wersji językowej interfejsu użytkownika środowiska programistycznego Microsoft Visual Studio Express

Zmiany wersji językowej nie można dokonać w trakcie pracy środowiska. Po kliknięciu przycisku OK na ekranie pojawi się pole dialogowe informujące o konieczności zamknięcia wszystkich okien (w szczególności okna głównego) oraz ponownego uruchomienia środowiska (rysunek 1.19). Dopiero po wykonaniu tych czynności interfejs użytkownika zacznie być wyświetlany w języku polskim. Rysunek 1.19. Zmiana wersji językowej wymaga zamknięcia i ponownego otwarcia środowiska

Rozdział 1.  Środowisko programistyczne Microsoft Visual Studio Express 2012

33

Rejestracja Mimo że środowisko Microsoft Visual Studio Express jest udostępniane za darmo, korzystanie z niego przez ponad 30 dni wymaga zarejestrowania się i uzyskania indywidualnego numeru seryjnego, powiązanego z konkretnym użytkownikiem. Rejestracja jest darmowa, wymaga jednak podania dosyć dokładnych danych identyfikujących użytkownika (w tym: regularnie używanego adresu poczty elektronicznej, na który przysyłane będą powiadomienia) oraz wypełnienia ankiety. Aby rozpocząć rejestrację, z menu Pomoc należy wybrać pozycję Zarejestruj produkt. Na ekranie pojawi się pole dialogowe umożliwiające wprowadzenie numeru seryjnego produktu (rysunek 1.10). By uzyskać nowy numer, należy kliknąć odnośnik Zarejestruj w trybie online, otwierający okno przeglądarki prezentujące stronę WWW rejestracji do systemu Microsoft Live ID (rysunek 1.20). Osoby posiadające już konto Microsoft Live mogą wpisać swój adres poczty elektronicznej i hasło do odpowiednich pól formularza i kliknąć przycisk Zaloguj; pozostali użytkownicy muszą przejść przez procedurę zakładania nowego konta, rozpoczynaną kliknięciem odnośnika Utwórz konto teraz9.

Rysunek 1.20. System uwierzytelniania użytkowników Microsoft Live

9

Ta książka nie obejmuje opisu procedury zakładania konta Microsoft Live.

Część I  Środowisko pracy

34

Po zalogowaniu się w oknie przeglądarki zostanie wyświetlona ankieta użytkownika środowiska programistycznego Microsoft Visual Studio Express 2012 for Windows Desktop (rysunek 1.21). Aby otrzymać numer seryjny, należy poprawnie wypełnić wymagane pola oznaczone gwiazdką. Rysunek 1.21. Pierwsze pola ankiety użytkownika

Jeżeli zawartość ankiety zostanie zaakceptowana przez system ankietyzacji, kolejna strona wyświetlana w oknie przeglądarki będzie zawierała indywidualny numer seryjny programu przydzielony użytkownikowi (rysunek 1.22). Ten sam numer zostanie wysłany również na podany adres poczty elektronicznej. Mimo to warto zapisać sobie numer seryjny w osobnym pliku lub materiałach pomocniczych, by nie musieć przechodzić ponownie całej procedury rejestracji, np. w przypadku całkowitej utraty wszystkich danych z pamięci masowej komputera.

Rysunek 1.22. Strona WWW prezentująca numer seryjny produktu przydzielony użytkownikowi środowiska

Rozdział 1.  Środowisko programistyczne Microsoft Visual Studio Express 2012

35

Uzyskany numer można teraz wprowadzić w polu dialogowym rejestracji (rysunek 1.10). Ponieważ rejestracja obejmuje wszystkie konta użytkowników komputera, kliknięcie przycisku Następna spowoduje pojawienie się ciemnej planszy systemu UAC wymagającej potwierdzenia globalnej zmiany parametrów systemu. Po kliknięciu przycisku Tak program zostanie zarejestrowany, czego potwierdzeniem będzie zmiana wyglądu pola dialogowego rejestracji (rysunek 1.23). Rysunek 1.23. Środowisko Microsoft Visual Studio zostało poprawnie zarejestrowane

Tworzenie nowego projektu Środowisko Microsoft Visual Studio — również w darmowej wersji Express — może służyć do tworzenia bardzo dużych programów, składających się z wielu odrębnych lub powiązanych ze sobą modułów. Dodatkowo, poszczególne składniki mogą korzystać z tych samych plików składowych. Aby ułatwić pracę nad tak dużymi programami, wprowadzono cztery poziomy abstrakcji:  rozwiązanie10 (ang. solution) — grupuje wiele projektów, z których każdy może

być odrębnym programem lub biblioteką podprogramów; te składniki mogą być niezależne lub zależne od siebie;

10

Niestety, zastosowane przez firmę Microsoft rozwiązanie jest nieelegancką kalką językową. Element nazwany „rozwiązaniem” jest tak naprawdę projektem, na który składa się jeden lub więcej składników projektu (nazywanych tutaj „projektami”). Angielskie słowo solution, od którego wzięło się polskie rozwiązanie, nie zostało tutaj właściwie przetłumaczone na język polski.

Część I  Środowisko pracy

36

 projekt (ang. project) — pojedynczy program lub biblioteka podprogramów;  filtr (ang. filter) — grupuje zbiory źródłowe, umożliwiając szybkie odszukanie

interesującego programistę zbioru lub logiczne pogrupowanie zbiorów składających się na jakiś moduł programu;  zbiór (ang. file) — zawiera fragment kodu źródłowego programu, nagłówków

biblioteki podprogramów lub zasobów graficznego interfejsu użytkownika. Aby stworzyć nowy projekt składający się z jednego składnika, należy z menu Plik wybrać pozycję Nowy projekt. Na ekranie pojawi się w tym momencie pole dialogowe Nowy projekt (rysunek 1.24), prezentujące listę szablonów projektów. Na znajdującej się na lewym skraju okna liście kategorii projektu należy rozwinąć kategorię Zainstalowane i kategorie podrzędne Szablony oraz Visual C++, po czym wskazać pozycję Win32. Następnie w środkowym panelu okna należy podświetlić pozycję Aplikacja konsoli Win32. W ten sposób wskazuje się środowisku chęć stworzenia nowego projektu odpowiadającego aplikacji pisanej w języku C++ (bez wykorzystania rozszerzeń nadzorowanego języka programowania C++ wprowadzonych przez firmę Microsoft), wykonywanej w oknie tekstowej konsoli systemu.

Rysunek 1.24. Pole dialogowe tworzenia nowego projektu

W razie potrzeby można też określić miejsce w pamięci masowej, w którym zostanie stworzony folder przechowujący wszystkie zbiory składające się na projekt. Bazowa ścieżka dostępu jest wyświetlana w polu zatytułowanym Lokalizacja i może być zmo-

Rozdział 1.  Środowisko programistyczne Microsoft Visual Studio Express 2012

37

dyfikowana ręcznie lub przez wskazanie folderu w polu dialogowym pojawiającym się po kliknięciu przycisku Przeglądaj. Dodatkowo zaznaczenie pola Utwórz katalog dla rozwiązania nakazuje środowisku stworzenie folderu dla całego rozwiązania, a w nim folderów dla poszczególnych projektów (wyczyszczenie pola powoduje, że stworzony zostanie jedynie folder projektu, co może być przydatne — upraszczając strukturę folderów w pamięci masowej — w przypadku tworzenia prostych rozwiązań, składających się wyłącznie z jednego projektu). Na koniec należy określić nazwę rozwiązania oraz tworzonego od razu pierwotnego projektu. Nazwę całego rozwiązania należy wpisać w polu zatytułowanym Nazwa rozwiązania11, zaś nazwę projektu — w polu Nazwa. Jeżeli jako pierwsza zostanie wprowadzona nazwa projektu, ta sama nazwa automatycznie stanie się początkową nazwą rozwiązania12. Po kliknięciu przycisku OK pole dialogowe Nowy projekt zostanie zastąpione oknem kreatora nowego składnika projektu, zatytułowanym Kreator aplikacji Win32. Pierwsza plansza odgrywa wyłącznie rolę informacyjną (rysunek 1.25) i należy ją pominąć, klikając przycisk Następny. Dopiero kolejna plansza, z podtytułem Ustawienia aplikacji, zawiera pola wpływające na tworzony składnik projektu (rysunek 1.26). Rysunek 1.25. Kreator tworzenia nowego projektu

W grupie pól zatytułowanej Typ aplikacji powinna być już zaznaczona pozycja Aplikacja konsolowa i nie należy tego zmieniać. Trzeba jednak usunąć znaczniki z pól Nagłówek

11

Wprowadzenie nazwy rozwiązania jest zbędne w przypadku rezygnacji z tworzenia folderu rozwiązania (usunięcia znacznika z pola Utwórz katalog dla rozwiązania). 12 Oczywiście tak automatycznie ustawioną nazwę rozwiązania można jeszcze zmodyfikować przed kliknięciem przycisku OK.

Część I  Środowisko pracy

38 Rysunek 1.26. Opcje projektu

kompilowany wstępnie13 i Testy Cyklu projektowania zabezpieczeń (SDL) oraz zaznaczyć pole Pusty projekt. Dzięki temu stworzony pierwotny projekt rozwiązania nie będzie zawierał żadnych zbiorów, dając programiście wolną rękę w dodawaniu i tworzeniu zbiorów z tekstem źródłowym programu. Po kliknięciu przycisku Zakończ nowe rozwiązanie zostanie stworzone i zapisane w pamięci masowej w miejscu wybranym w polu dialogowym z rysunku 1.24, a w panelu Eksplorator rozwiązania, prezentującym rozwiązanie i jego składniki (rysunek 1.27), pojawi się nazwa rozwiązania (Rozwiązanie „Projekt1”) oraz pierwotnego projektu (Projekt1). Panel Eksplorator rozwiązania umożliwia nawigację w ramach całego rozwiązania, przeglądanie projektów składowych oraz dodawanie i tworzenie zbiorów składających się na każdy z projektów.

Tworzenie zbiorów tekstu źródłowego programu Tekst źródłowy programu zapisuje się w zbiorach dyskowych powiązanych ze sobą w ramach projektów. W każdym projekcie są zakładane automatycznie cztery filtry, w których należy umieszczać własne zbiory lub zasoby:

13

Jeżeli się tego nie zrobi, dyrektywy preprocesora #include zapisywane na początku każdego zbioru z kodem źródłowym będą musiały być zapisywane w określonej kolejności i z wykorzystaniem standardowo tworzonego pliku nagłówkowego. Niedotrzymanie tego warunku powoduje trudne do zdiagnozowania błędy kompilacji. Ponieważ mechanizm prekompilacji zbiorów nagłówkowych w przypadku prostych programów nie daje większych oszczędności, najprościej jest go wyłączyć, usuwając znacznik z wymienionego pola.

Rozdział 1.  Środowisko programistyczne Microsoft Visual Studio Express 2012

39

Rysunek 1.27. Środowisko Microsoft Visual Studio ze świeżo utworzonym jednoskładnikowym rozwiązaniem  Pliki nagłówkowe — przechowuje tzw. zbiory nagłówkowe, zawierające

deklaracje podprogramów i klas znajdujących się w używanych bibliotekach podprogramów oraz we własnych zbiorach z tekstem źródłowym; zbiór nagłówkowy jest niezbędny, by kompilator w czasie przetwarzania tekstu źródłowego z jednego zbioru dysponował informacją o elementach zapisanych w innym zbiorze;  Pliki zasobów — przechowuje zbiory zasobów graficznego interfejsu

użytkownika, wykorzystywane przy tworzeniu aplikacji graficznych Win32; w przypadku opisywanych w tej książce programów korzystających z tekstowego środowiska pracy, folder ten będzie zawsze niewykorzystany;  Pliki źródłowe — przechowuje zbiory z tekstem źródłowym programu;  Zależności zewnętrzne — zawiera listę projektów i zbiorów, od których zależy

dany projekt (a więc: które muszą zostać poddane kompilacji lub sprawdzone, zanim rozpocznie się kompilacja danego projektu). Aby stworzyć nowy zbiór z tekstem źródłowym, należy kliknąć prawym przyciskiem myszy na nazwie filtru Pliki źródłowe (rysunek 1.28). Następnie w menu kontekstowym należy podświetlić pozycję Dodaj i w kolejnym poziomie menu kliknąć pozycję Nowy element. Na ekranie pojawi się pole dialogowe Dodaj nowy element (rysunek 1.29), umożliwiające utworzenie nowego zbioru z tekstem źródłowym programu i umieszczenie go w podświetlonym filtrze (tu: Pliki źródłowe) wybranego projektu (tu: Projekt1).

40

Część I  Środowisko pracy

Rysunek 1.28. Dodawanie nowego zbioru z tekstem źródłowym programu do projektu

Rysunek 1.29. Wybór typu i nazwy tworzonego zbioru z tekstem źródłowym

W oknie tym należy w lewym panelu wybrać kategorię właściwą dla zbioru (pozycja Kod w kategorii Visual C++), a w środkowym podświetlić typ zbioru (Plik C++ (.cpp); typ ten jest właściwy dla tekstu źródłowego programów pisanych w językach C i C++). Następnie w polu zatytułowanym Nazwa należy wpisać nazwę zbioru, pod

Rozdział 1.  Środowisko programistyczne Microsoft Visual Studio Express 2012

41

jaką będzie on występował w pamięci masowej. Należy przy tym pamiętać o dołączeniu właściwego rozszerzenia nazwy pliku:  .c — jeżeli tworzymy zbiór zawierający tekst źródłowy programu w języku C;  .cpp — jeżeli tworzymy zbiór zawierający tekst źródłowy programu

w języku C++;  .h — jeżeli tworzymy zbiór nagłówkowy opisujący elementy zapisane

w językach C lub C++ (bez różnicy)14.

Na przykład aby stworzyć pusty zbiór z tekstem źródłowym programu w języku C, w polu Name można wpisać nazwę Program.c. Domyślnie zbiory należące do projektu umieszczane są w pamięci masowej w folderze należącym do tego projektu. Używana ścieżka jest wyświetlana w polu zatytułowanym Lokalizacja. W razie potrzeby tę ścieżkę można zmienić albo ręcznie (w polu Lokalizacja), albo wybierając dowolny folder w polu dialogowym pojawiającym się po kliknięciu przycisku Przeglądaj. Kliknięcie przycisku Dodaj spowoduje utworzenie nowego zbioru, zapisanie go w pamięci masowej i dodanie do filtru Pliki źródłowe projektu. Zbiór zostanie też od razu otwarty do edycji (rysunek 1.30).

Rysunek 1.30. Nowo utworzony zbiór z tekstem źródłowym programu w języku C 14

Czasem dla odróżnienia zbiorów nagłówkowych dla modułów napisanych w języku C++ stosuje się rozszerzenia nazw plików .hh lub .hpp.

Część I  Środowisko pracy

42

Kompilacja rozwiązania Aby uruchomić program, należy dokonać kompilacji rozwiązania, a więc przetłumaczenia tekstu źródłowego wszystkich projektów w ramach rozwiązania na postać maszynową (kod maszynowy). Dopiero po pomyślnym zakończeniu kompilacji można podjąć próbę uruchomienia programu. Do testowania opcji kompilacji projektu można użyć programu przedstawionego na listingu 1.1. Program o takim tekście źródłowym wyświetla prosty komunikat i oczekuje naciśnięcia dowolnego klawisza. Listing 1.1. Program przykładowy #include #include int main() { printf("Oto efekt dzialania programu!\n"); printf("(nacisnij dowolny klawisz aby zakonczyc)\n"); _getch(); return 0; }

Po wpisaniu tekstu programu należy otworzyć menu Kompilacja i kliknąć w nim pozycję Kompiluj rozwiązanie. Program zostanie skompilowany, a o postępie tego procesu będą świadczyły informacje wyświetlane przez kompilator w panelu Dane wyjściowe okna środowiska pracy (rysunek 1.31). Jeżeli kompilacja powiedzie się (w podsumowaniu zostanie wyświetlony komunikat zawierający tekst 0 zakończono niepowodzeniem), można przystąpić do uruchomienia programu, wybierając z menu Debuguj15 pozycję Start debugging16. Program będzie wyświetlał wyniki swojego działania w odrębnym oknie (rysunek 1.32). Po naciśnięciu dowolnego klawisza (kończy to działanie tego programu) okno programu zostanie automatycznie zamknięte, a środowisko Visual C++ z trybu nadzorowania działania programu17 przejdzie ponownie do trybu edycji tekstu źródłowego.

15

I to również nie jest najszczęśliwszym tłumaczeniem angielskiego terminu debug, oznaczającego skrótowo usuwanie błędu z programu. W polskiej nomenklaturze informatycznej przyjęło się nazywać tę fazę tworzenia programu uruchamianiem, a angielskie słowo debugger oznaczające program diagnostyczny wspomagający proces uruchamiania (takim programem jest również środowisko Visual Studio) — programem uruchomieniowym. 16 To nie jest pomyłka. Niestety, pakiet polonizujący środowisko Microsoft Visual Studio Express zawiera kilka niedoskonałości polegających na braku tłumaczenia wybranych elementów interfejsu użytkownika. 17 Trybu uruchomieniowego.

Rozdział 1.  Środowisko programistyczne Microsoft Visual Studio Express 2012

43

Rysunek 1.31. Efekt pomyślnej kompilacji programu przykładowego Rysunek 1.32. Tekst wyświetlany przez program po uruchomieniu

Skrótem klawiszowym umożliwiającym szybkie wywołanie kompilacji rozwiązania (odpowiednik pozycji Kompiluj rozwiązanie z menu Kompilacja) jest F7. Z kolei uruchomienie programu (pozycja Start debugging menu Debuguj) można uzyskać, naciskając klawisz F5.

Jeżeli część opcji środowiska programistycznego wydaje się niedostępna, gdzieś w tle może być zagubione okno uruchamianego programu. Aby wymusić przerwanie programu, można z menu Debuguj wybrać pozycję Stop debugging lub nacisnąć odpowiadającą jej kombinację klawiszy Shift+F5.

Część I  Środowisko pracy

44

W wersji 2012 środowiska Microsoft Visual Studio wprowadzono pożyteczną funkcję umożliwiającą łatwe zorientowanie się, czy w danym momencie środowisko znajduje się w trybie edycji, czy uruchamiania programu. W trybie edycji wiersz stanu u dołu okna jest błękitny, a w trybie uruchamiania pomarańczowy.

Wybór wariantu kompilacji W języku C możliwa — i szeroko wykorzystywana — jest tzw. kompilacja wariantowa. Polega ona na generowaniu kodu zgodnie z różnymi kryteriami w zależności od wybranego wariantu. Możliwe jest również — dzięki dyrektywom preprocesora — dołączanie lub wyłączanie niektórych fragmentów kodu w wybranych wariantach. Środowisko Microsoft Visual Studio w ramach każdego tworzonego rozwiązania udostępnia standardowo dwa warianty kompilacji:  Debug — wariant aktywny zaraz po stworzeniu rozwiązania. Odpowiada

wersji programu przeznaczonej do uruchamiania pod kontrolą programu uruchomieniowego18, pozwalającego programiście na śledzenie toku realizacji programu, podglądanie zawartości pamięci i wykrywanie błędów. Wariant Debug skompilowanego programu nie jest poddawany optymalizacji, jest zazwyczaj wyraźnie obszerniejszy i wolniejszy; nie nadaje się zatem do rozpowszechniania jako gotowy produkt. W tej wersji są ponadto obecne dodatkowe fragmenty kodu wykrywające niektóre kategorie błędów, co jeszcze poważniej zmniejsza wydajność programu. Tego wariantu kompilacji należy koniecznie używać podczas tworzenia i rozwijania programu, ponieważ zdecydowanie ułatwia diagnostykę programu i usuwanie błędów.  Release — wariant wybierany na żądanie. Odpowiada finalnej wersji programu,

przeznaczonej do dystrybucji. W wariancie tym aktywny jest optymalizator, który kosztem dłuższej kompilacji rozwiązania umożliwia zmniejszenie rozmiaru programu i zwiększenie szybkości jego działania; powoduje to jednak, że zoptymalizowany program trudniej jest diagnozować i kontrolować jego działanie. Usuwane są też wszystkie fragmenty kodu odpowiedzialne za wykrywanie i lokalizowanie błędów. Tego wariantu kompilacji należy używać w celu wygenerowania wersji programu przekazywanej odbiorcom końcowym. Wyboru wariantu kompilacji dokonuje się z poziomu głównego paska narzędziowego środowiska, wskazując odpowiednią pozycję na rozwijanej liście (rysunek 1.33). Bezpośrednio po wyborze wariantu należy ponownie uruchomić kompilację, wybierając z menu Kompilacja pozycję Kompiluj rozwiązanie lub naciskając klawisz F7. Jeżeli przed zmianą wariantu program poddawany był poważnym przeróbkom, warto wręcz

18

Ang. debugger (dosłownie: odpluskwiacz).

Rozdział 1.  Środowisko programistyczne Microsoft Visual Studio Express 2012

45

Rysunek 1.33. Rozwijana lista wariantów kompilacji programu

zlecić usunięcie wcześniej skompilowanych modułów wynikowych i ponowną kompilację całości rozwiązania; można to osiągnąć przez wybranie z menu Kompilacja pozycji Kompiluj ponownie rozwiązanie. Ponowna kompilacja całości rozwiązania może pomóc w rozwiązaniu problemów występujących w czasie zwykłej kompilacji przyrostowej i wynikających z uszkodzenia zbiorów zawierających moduły wynikowe poszczególnych części tworzonego programu.

W każdym z wariantów kompilacji tworzony program będzie zależny od dynamicznie dołączanej biblioteki19 podprogramów standardowych języków C i C++. Z tego powodu przeniesienie samego pliku wykonywalnego .exe na inny komputer nie musi oznaczać możliwości uruchomienia programu. Pewną opcją jest zainstalowanie pakietu bibliotek kompilatora Microsoft Visual Studio20, który można pobrać z serwisu WWW firmy Microsoft, jednak lepszym rozwiązaniem jest uniezależnienie programu od tych bibliotek i dołączenie ich — wyłącznie w wariancie Release — do kodu wykonywalnego programu. Aby tego dokonać, należy wybrać wariant kompilacji Release, a następnie z menu Projekt wybrać pozycję Właściwości (nazwa projektu). Na ekranie pojawi się pole dialogowe (Nazwa projektu) Strony właściwości (rysunek 1.34), prezentujące wszystkie ustawienia związane z bieżącym projektem21. Na liście kategorii znajdującej się po lewej stronie okna należy rozwinąć kolejno gałęzie: Właściwości konfiguracji oraz C/C+, po czym na liście ustawień po prawej stronie okna należy odszukać pozycję Biblioteka środowiska uruchomieniowego, rozwinąć ją i zastąpić standardowy wybór Wielowątkowy DLL pozycją Wielowątkowy. Po zatwierdzeniu wyboru przez kliknięcie przycisku OK i ponownym skompilowaniu rozwiązania uzyskany plik wykonywalny .exe będzie można bez ograniczeń przenosić na inne komputery i uruchamiać bez konieczności wcześniejszego instalowania zestawów dodatkowych bibliotek.

Usuwanie błędów kompilacji Jeżeli w czasie kompilacji pojawią się błędy22, zamiast podsumowania procesu kompilacji rozwiązania u dołu okna pojawi się panel listy błędów (rysunek 1.35). Błędnego programu nie wolno próbować uruchamiać; próba uruchomienia w takim przypadku spowoduje 19

DLL, ang. dynamic link library. Microsoft Visual C++ Redistributable Package. 21 W przypadku rozwiązania zawierającego wiele projektów te czynności należy powtórzyć dla każdego projektu składowego z osobna. 22 Błędy znajdowane przez kompilator noszą nazwę błędów składniowych. Ich przyczyną jest zawsze wprowadzenie przez programistę błędnego tekstu źródłowego programu. 20

46

Część I  Środowisko pracy

Rysunek 1.34. Usunięcie zależności skompilowanej wersji programu (w wariancie Release) od dynamicznie dołączanych bibliotek kompilatora Microsoft Visual Studio

pojawienie się na ekranie pola dialogowego z informacją, że projekt nie został pomyślnie skompilowany (rysunek 1.36). Kliknięcie przycisku Tak spowoduje powtórną kompilację projektu, co oczywiście — skoro błąd nie został usunięty — znów skończy się błędem. Kliknięcie przycisku Nie spowoduje uruchomienie ostatniej poprawnej wersji programu23, co oczywiście uniemożliwia testowanie zmian wprowadzonych do programu od czasu ostatniej kompilacji. Jedynym sensownym rozwiązaniem jest skorzystanie z przycisku Anuluj powodującego odwołanie chęci uruchomienia programu i poprawienie tekstu źródłowego programu. Podwójne klikanie pozycji listy błędów powoduje wyświetlenie zawartości właściwego zbioru z tekstem źródłowym programu i ustawienie karetki w wierszu, w którym wystąpił błąd. Najczęściej błąd będzie dodatkowo podkreślony w tekście źródłowym czerwoną falowaną linią. Ułatwia to zlokalizowanie i usunięcie błędu. Poprawiając błędy składniowe, należy pamiętać o następujących zasadach:  Błędy mają najczęściej charakter kaskadowy. Jeden realny błąd może

spowodować wygenerowanie fałszywej informacji o obecności kolejnych błędów w dalszej części tekstu źródłowego programu. Z tego powodu zawsze należy rozpoczynać usuwanie błędów od pierwszego błędu znalezionego w tekście 23

Jeżeli oczywiście program choć raz został skompilowany bez wystąpienia żadnego błędu składniowego.

Rozdział 1.  Środowisko programistyczne Microsoft Visual Studio Express 2012

47

Rysunek 1.35. Kompilacja programu zakończyła się błędem; środowisko programistyczne reaguje na to wyświetleniem panelu z listą błędów Rysunek 1.36. Towarzysząca próbie uruchomienia programu informacja o błędach podczas kompilacji, uniemożliwiających uruchomienie

źródłowym, a po jego usunięciu warto ponownie przeprowadzić kompilację, gdyż w wielu przypadkach fałszywe informacje o błędach nie zostaną już wtedy wygenerowane.  Błędy są czasem zgłaszane nie w błędnym wierszu, lecz w wierszach kolejnych.

Jeżeli zatem na pierwszy rzut oka nie widać błędu w miejscu wskazanym przez kompilator, warto przeglądnąć też kilka wierszy poprzedzających ten wskazany.

Część I  Środowisko pracy

48

Zapisywanie zmian w tekście źródłowym Częste zapisywanie zmian w tekście źródłowym programu stanowi zabezpieczenie na wypadek zablokowania się (lub wyłączenia) komputera. Zdarza się również, że awarii ulegnie samo środowisko programistyczne, co pociąga za sobą utratę wszystkich zmian wprowadzonych w tekście źródłowym od momentu ostatniego zapisu. Dlatego istotne jest regularne zapisywanie wprowadzonych zmian, do czego służy pozycja Zapisz w menu Plik. Ponieważ ciągłe sięganie do menu okna w celu zapisania zmian byłoby niewygodne, w praktyce zawsze korzysta się z kombinacji klawiszy Ctrl+S. Możliwe jest również zapisanie wszystkich zmodyfikowanych zbiorów, które są w danym momencie otwarte do edycji. Służy do tego pozycja Zapisz wszystko menu Plik, której odpowiada z kolei skrót klawiszowy Ctrl+Shift+S. W pewnym sensie przed utratą zmian chroni też częste wywoływanie funkcji kompilacji programu. Przed każdą kompilacją środowisko programistyczne zapisuje wszystkie zmiany wprowadzone w edytowanych zbiorach24. Opłaca się zatem regularnie kompilować pisane programy — pozwala to już na wczesnym etapie pracy usuwać wszystkie błędy składniowe (póki ich liczba jest względnie niewielka) i zabezpiecza przed stratą większego fragmentu kodu. Warto jednak również zaszczepić w sobie zwyczaj regularnego korzystania z kombinacji klawiszy Ctrl+S, zapisującej wprowadzone zmiany. Otwarte do edycji zbiory, w których wprowadzono niezapisane jeszcze zmiany, oznaczone są w polu edycyjnym okna środowiska programistycznego gwiazdką dołączoną do nazwy zbioru wyświetlanej w nagłówku zakładki edytora (rysunek 1.37).

Rysunek 1.37. Gwiazdka informuje o zmianach wprowadzonych w edytowanym zbiorze

Uruchamianie programu spoza środowiska Skompilowaną wersję programu można uruchomić bez obecności środowiska Microsoft Visual Studio w pamięci komputera. Aby to uczynić, należy odszukać folder, w którym zapisany został projekt (rysunek 1.38). W czasie kompilacji rozwiązania tworzone są w nim foldery podrzędne odpowiadające wybranemu wariantowi kompilacji: Debug 24

Kompilator jest w stanie operować jedynie na tekście źródłowym programu zapisanym w zbiorach w pamięci masowej — nie ma dostępu do tekstu źródłowego w postaci obecnej w edytorze środowiska programistycznego. Z tego powodu, by kompilacji podlegał aktualny tekst źródłowy programu, w momencie uruchomienia kompilacji środowisko najpierw zapisuje wszystkie zmiany w pamięci masowej, a dopiero później przekazuje sterowanie kompilatorowi.

Rozdział 1.  Środowisko programistyczne Microsoft Visual Studio Express 2012

49

Rysunek 1.38. Foldery Debug i Release zawierają wykonywalną wersję programu w postaci zbioru .exe

lub Release25. W nich umieszczany jest również zbiór wykonywalny .exe reprezentujący program. Po wejściu do jednego z wymienionych folderów można ten zbiór uruchomić (co odpowiada uruchomieniu programu) lub skopiować w dowolne inne miejsce.

Kończenie pracy Pracę w środowisku programistycznym można zakończyć na trzy sposoby:  w menu Plik wybrać pozycję Zakończ;  zamknąć okno aplikacji Microsoft Visual Studio;  skorzystać ze skrótu klawiszowego Alt+F4.

Niezależnie od wybranej metody, jeżeli w czasie zamykania środowiska programistycznego uruchomiony jest własny program programisty, na ekranie pojawi się pole dialogowe z pytaniem o zamiar przerwania pracy tego programu (rysunek 1.39). Kliknięcie przycisku Tak spowoduje zamknięcie zarówno uruchamianego programu, jak i środowiska programistycznego. Z kolei przycisk Nie pozwala anulować zamykanie środowiska i kontynuować pracę. Rysunek 1.39. Pytanie o chęć zakończenia pracy własnego programu

25

Lub obydwa.

50

Część I  Środowisko pracy

Część II

Język C Pierwsza część kursu poświęcona jest językowi C. Znajomość języka C wprost przekłada się na umiejętność programowania w języku C++: w obu językach identyczna jest składnia, takie same podstawowe instrukcje, identyczne zasady przechowywania i przetwarzania danych. Oczywiście, zmieniają się reguły, wedle których należy budować program i dzielić go na moduły, jednak przyswojenie sobie nowych reguł staje się o wiele prostsze, gdy składnia języka nie stanowi tajemnicy, a wszystkie konstrukcje językowe składające się na konkretne operacje arytmetyczno-logiczne są zrozumiałe i oczywiste. W ramach tej części książki przedstawione zostaną następujące zagadnienia:  wprowadzanie danych z klawiatury, wyprowadzanie informacji na ekran

oraz przechowywanie danych w pamięci operacyjnej komputera;  przechowywanie dużych zbiorów danych w postaci tablic;  przetwarzanie zbiorów danych w sposób automatyczny z wykorzystaniem pętli;  wybiórcza realizacja operacji, uzależniona od określonych przez programistę

warunków;  tworzenie własnych podprogramów i bibliotek podprogramów;  tworzenie wyliczeniowych typów danych;  tworzenie własnych strukturalnych typów danych, składających się z wielu

elementów podrzędnych;  przetwarzanie tekstów;  wymiana danych z innymi programami;  przechowywanie danych w pamięci masowej (tworzenie zbiorów danych,

zapisywanie danych w zbiorze i odczytywanie danych z powrotem z pamięci masowej do pamięci operacyjnej);  dynamiczna gospodarka pamięcią operacyjną oraz operowanie na danych

lokalizowanych za pomocą zmiennych wskaźnikowych.

52

Część II  Język C

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

53

Rozdział 2.

Wprowadzanie, wyprowadzanie i przechowywanie danych Większość użytecznych programów wymaga wprowadzenia zestawu danych poddawanych obróbce i prezentuje wyniki swojej pracy. Pobieranie i wyprowadzanie danych może być ukryte (dane mogą być przekazywane ze zbiorów zapisanych w pamięci masowej i do nich, a także wymieniane z innymi programami przez potoki transmisyjne) lub jawne. W tym drugim przypadku program prosi użytkownika o wprowadzenie danych, przetwarza je i przedstawia wyniki na ekranie komputera. W rozdziale zaprezentowane zostaną następujące zagadnienia:  podprogram główny main(), od którego rozpoczyna się działanie prawie każdego

programu napisanego w językach C i C++;  wyrażenie return kończące działanie programu;  instrukcja printf() realizująca wyprowadzenie napisów i danych na ekran;  typy i deklaracje zmiennych, dzięki którym można zapisać w pamięci komputera

wprowadzone wartości i skorzystać z nich w dalszych wierszach programu;  pobieranie danych z klawiatury za pomocą instrukcji scanf() i przechowywanie

ich w utworzonych wcześniej zmiennych.

Podprogram główny Zaraz po rozpoczęciu realizacji programu system operacyjny nakazuje mikroprocesorowi wykonywać kolejne rozkazy składowe tego programu. Aby było to możliwe, w programie musi zostać oznaczone miejsce, od którego za każdym razem ma zostać

Część II  Język C

54

rozpoczęta realizacja. W językach C i C++ miejscem takim jest podprogram główny, noszący nazwę main()1. W kolejnych rozdziałach książki zostanie wyjaśnione, czym są podprogramy2 i jak się je zapisuje. Tymczasem trzeba przyjąć, że podprogram główny main()wygląda następująco: int main() { }

Języki C i C++ rozróżniają małe i wielkie litery. Nie wolno zapisywać tekstu programu w sposób dowolny: jeżeli jakieś wyrażenie składa się z małych liter, należy wprowadzić je małymi literami; z kolei jeżeli niektóre lub wszystkie litery wyrażenia są wielkie, takie muszą zostać wprowadzone do programu.

Pierwszy z trzech wierszy przedstawionego powyżej kodu oznacza początek podprogramu main(). Kolejny wiersz, zawierający otwierający nawias klamrowy {, oznacza początek bloku instrukcji programu: za nim dopisuje się kolejne instrukcje noszące nazwę wyrażeń języka C++. Zamykający nawias klamrowy } kończy podprogram main() i oznacza, że dalej nie mogą się znaleźć żadne instrukcje. Strukturę bloku instrukcji otoczonego dwoma nawiasami klamrowymi należy dobrze zapamiętać, gdyż będzie ona wykorzystywana wielokrotnie w wielu konstrukcjach programowych. Używana jest wszędzie, gdzie trzeba wskazać kompilatorowi języka C lub C++, że otoczone klamrami wyrażenia powinien traktować jako jedną całość. Powyższy składający się z trzech wierszy przykład jest już w zasadzie poprawnym programem w języku C (a przy okazji również w języku C++). By jednak stał się całkowicie poprawny, między klamrami należy umieścić przynajmniej jedną instrukcję, którą musi być wyrażenie return. Gotowy program został przedstawiony na listingu 2.1. Listing 2.1. Najkrótszy program w języku C int main() { return 0; }

Wyrażenie return powoduje wyjście z podprogramu głównego main(), co odpowiada zakończeniu pracy całego programu. Zero umieszczone obok wyrazu return oznacza, że system operacyjny — oczekujący zawsze po zakończeniu programu na informację o tym, z jakiego powodu program zakończył pracę — otrzyma właśnie wartość zero. Kody wyniku działania programu są istotne jedynie dla innych programów: ponieważ trudno się spodziewać, by z przedstawionym powyżej banalnym programem współ1 2

Z ang. main — główny. W skrócie: podprogram to wydzielony fragment programu otoczony nawiasami klamrowymi, z nadaną unikatową nazwą. Podprogram może przyjmować dane wejściowe, realizować jakieś operacje i zwracać dowolny wynik. Podprogram nie jest nigdy wykonywany sam z siebie: musi zostać uruchomiony przez podanie jego nazwy i danych wejściowych (jeżeli są potrzebne). W przypadku podprogramu głównego main(), jest on wywoływany (pośrednio) przez sam system operacyjny.

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

55

pracowała jakakolwiek inna aplikacja, można by zwrócić dowolną liczbę całkowitą. Zwyczajowo przyjęło się jednak, że normalne, poprawne zakończenie pracy programu oznacza się właśnie zerem. Zaraz za zerem znajduje się średnik, który w językach C oraz C++ kończy każde wyrażenie. Opuszczenie go spowoduje pojawienie się w czasie kompilacji mnóstwa rozmaitych błędów, których zidentyfikowanie może być niezwykle trudne — należy zatem zawsze zwracać szczególną uwagę, by wszystkie wyrażenia były poprawnie zakończone średnikami. Aby wpoić sobie korzystanie ze średnika, warto rozumieć go jako wyraz „wykonaj”, kończący instrukcję lub wyrażenie — bez niego kompilator nie będzie wiedział, że ma wykonać znajdujące się przed średnikiem wyrażenie i przejść do realizacji kolejnego i będzie bezskutecznie próbował złączyć ze sobą dwa kolejne wyrażenia, nawet znajdujące się w dwóch różnych wierszach3. Należy pamiętać też, że umieszczanie jakichkolwiek wyrażeń za wyrażeniem return nie ma sensu. Po wykonaniu instrukcji return program natychmiast zakończy realizację podprogramu main(), ignorując wszystkie dalsze wyrażenia.

Umieszczanie tekstu na ekranie Czas spowodować, by program przemówił. Co prawda efektem nie będzie głos płynący z głośników, a tekst pojawiający się na ekranie, będzie to jednak zdecydowany postęp w stosunku do poprzedniego przykładu. Do wyprowadzania informacji na ekran służy podprogram printf(). W najprostszym przypadku jednym parametrem4, jaki należy podać wewnątrz nawiasów okrągłych jest tekst, który ma zostać wyświetlony. Prosty program wypisujący na ekranie jeden wiersz tekstu został przedstawiony na listingu 2.2. Listing 2.2. Przykład użycia podprogramu printf() int main() { printf("Ten tekst zaraz zostanie wyswietlony na ekranie!"); return 0; }

3

4

Ten problem jest rozwiązywany w różny sposób w różnych językach programowania. Na przykład w języku Google Go wyrażenia nie muszą kończyć się średnikiem, jednak jeżeli chce się umieścić w jednym wierszu kilka wyrażeń, należy je rozdzielić właśnie średnikiem. Istnieją też języki programowania, w których średnika nie daje się nigdy, jednak zabronione jest umieszczanie wielu wyrażeń w jednym wierszu lub dzielenie wyrażenia między wiersze. W większości języków programowania — w tym: C#, Java, C, C++, Vala — programiście pozostawia się dowolność rozmieszczania i dzielenia wyrażeń, wymagając jednak od niego w zamian kończenia każdego wyrażenia średnikiem. Parametrami nazywamy dane wejściowe podprogramu, przekazywane wewnątrz okrągłych nawiasów zapisywanych po nazwie podprogramu i rozdzielane przecinkami.

Część II  Język C

56

Najważniejsze punkty powyższego programu warte zauważenia i zapamiętania to:  wyrażenie printf() znajduje się przed wyrażeniem return, dzięki czemu

nie zostanie pominięte;  podprogram printf() jest tutaj wywołany z jednym parametrem; ten parametr

jest tekstem zamkniętym w znaki cudzysłowu i umieszczony został wewnątrz nawiasów okrągłych;  wyrażenie printf() kończy się średnikiem umieszczonym zaraz za zamykającym

listę parametrów nawiasem okrągłym;  każdy wiersz znajdujący się pomiędzy nawiasami klamrowymi zaczyna

się od kilku odstępów — nie ma to żadnego wpływu na działanie programu, niesamowicie ułatwia jednak czytanie tekstu programu: od razu można zauważyć, gdzie zaczyna się, a gdzie kończy blok instrukcji; np.: int main() { Wyrazenie1; Wyrazenie2; { Wyrazenie1_w_bloku_2; Wyrazenie2_w_bloku_2; { Wyrażenie_inne; } Wyrazenie3; }

Bloki instrukcji można swobodnie zagnieżdżać. Nie należy jednak nadmiernie korzystać z tej możliwości: najczęściej tylko zaciemnia to konstrukcję programu. Kolejne bloki powinno się rozpoczynać tylko w tych miejscach, gdzie jest to niezbędne. W dalszej części książki za każdym razem, gdy korzystanie z bloku jest wskazane lub konieczne, będzie to podkreślone.

Próba kompilacji powyższego programu pozornie zakończy się powodzeniem. Jeżeli jednak za pomocą pozycji Lista błędów menu Widok wyświetli się listę błędów, znajdzie się na niej pozycja informująca o tym, że kompilator nie jest pewien, czym jest wyrażenie printf() (rysunek 2.1). Dzieje się tak, gdyż słowo printf nie jest słowem kluczowym języka C. Słowa kluczowe to podstawowe elementy języka tworzące jego składnię i zawsze znane kompilatorowi. Jedynymi dwoma słowami kluczowymi użytymi dotychczas były int oraz return (main nie jest słowem kluczowym, jednak z omówionych w dalszych rozdziałach powodów nie powoduje tu problemu). Aby użyć podprogramu printf() należy dołączyć do programu jego deklarację znajdującą się w pliku stdio.h; tekst źródłowy programu z odpowiednią modyfikacją znajduje się na listingu 2.3. Wiersz #include nie składa się z elementów języka C. Jest to tzw. dyrektywa preprocesora nakazująca preprocesorowi — programowi analizującemu i modyfikującemu postać źródłową programu przed każdą kompilacją — wstawić w to miejsce

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

57

Rysunek 2.1. Komunikat kompilatora informujący o nieznajomości podprogramu printf() Listing 2.3. Pełna postać programu zawierająca deklarację umożliwiającą korzystanie z podprogramu printf() #include int main() { printf("Ten tekst zaraz zostanie wyswietlony na ekranie!"); return 0; }

kodu zawartość innego zbioru. W tym przypadku tym zbiorem jest stanowiący część biblioteki standardowej języka C (instalowanej wraz ze środowiskiem Microsoft Visual Studio) zbiór stdio.h (ang. Standard Input/Output) zawierający deklaracje wielu najważniejszych podprogramów bibliotecznych języków C i C++. Po wprowadzeniu tego dodatkowego wiersza kodu program kompiluje się bez problemu, a uruchomienie go powoduje pojawienie się napisu na ekranie. Problem polega na tym, że okno prezentujące efekt działania programu znika prawie od razu, uniemożliwiając zaobserwowanie efektów działania programu. Ten problem można rozwiązać, dodając w kolejnym wierszu za wywołaniem podprogramu printf() odwołanie do podprogramu _getch(), oczekującego naciśnięcia dowolnego klawisza i zwracającego kod tego klawisza. W efekcie program (którego pełny tekst źródłowy jest przedstawiony na listingu 2.4) wyświetli na ekranie pożądany napis (rysunek 2.2), poczeka na naciśniecie dowolnego klawisza i dopiero wtedy zakończy działanie, czego skutkiem będzie zamknięcie jego okna.

Część II  Język C

58 Listing 2.4. Modyfikacja programu oczekująca naciśnięcia dowolnego klawisza #include #include int main() { printf("Ten tekst zaraz zostanie wyswietlony na ekranie!"); _getch(); return 0; }

Rysunek 2.2. Efekt działania skompilowanego programu

Aby program skompilował się poprawnie, trzeba jednak pamiętać o dopisaniu odwołania do kolejnego zbioru nagłówkowego. Tym razem nosi on nazwę conio.h (ang. Console Input/Ouput). Kolejna przykra niespodzianka czeka programistę chcącego za pomocą kilku kolejnych wywołań podprogramu printf wyświetlić kilka wierszy tekstu (listing 2.5). Efektem działania programu będzie jeden długi wiersz tekstu (rysunek 2.3). Dzieje się tak, gdyż podprogram printf() nie dzieli w jakikolwiek sposób tekstu na wiersze. Może się to wydawać poważną wadą, jest jednak wielką zaletą: dzięki temu można połączyć dane wyprowadzane przez wiele oddzielnych instrukcji printf() w jeden wiersz tekstu i dopiero na sam koniec przejść do nowego wiersza lub wręcz przeciwnie — wyświetlić wiele wierszy tekstu za pomocą jednego wyrażenia.

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

59

Listing 2.5. Próba wyświetlenia na ekranie kilku oddzielnych wierszy tekstu #include #include int main() { printf("Ten tekst zaraz zostanie wyswietlony na ekranie!"); printf("Teraz drugi wiersz"); printf("a teraz trzeci!"); _getch(); return 0; }

Rysunek 2.3. Efektem realizacji kilku oddzielnych wyrażeń printf() jest jeden długi, zlepiony wiersz tekstu

Jest jednak sposób, by zmusić funkcję printf() nie tylko do przejścia do nowego wiersza, ale też do wykonania kilku innych operacji. Podprogram printf() interpretuje w sposób specjalny niektóre kombinacje znaków rozpoczynające się od znaku \ (odwrotny ukośnik, ang. backslash), umożliwiając zaszycie w kodzie programu informacji na temat formatowania tekstu. Najczęściej spotykane kombinacje zostały przedstawione w tabeli 2.1. Jeżeli teraz zmodyfikuje się program tak, aby na końcu każdego łańcucha tekstowego przekazywanego jako parametr podprogramu printf() umieszczona była kombinacja znaków \n: printf("Teraz drugi wiersz\n");

to po skompilowaniu i uruchomieniu programu wyprowadzony przez niego tekst będzie miał pożądany wygląd (rysunek 2.4).

Część II  Język C

60 Tabela 2.1. Specjalne kombinacje znaków rozpoczynające się od znaku \ Kombinacja

Kod ASCII

Znaczenie Cofnięcie aktualnej pozycji wstawiania znaków o jeden element5.

\b

8

\n

10

Wysunięcie wiersza. Pod kontrolą systemów operacyjnych napisanych przez firmę Microsoft korzystanie wyłącznie z tego kodu powinno powodować przejście do nowego wiersza, jednak bez powrotu do początku wiersza; ponieważ język C zakładał, że ten znak musi też powodować przejście do początku wiersza, biblioteki standardowe kompilatorów C i C++ dokonują tłumaczenia pojedynczego znaku \n na kombinację \r\n wymaganą przez systemy DOS i Windows.

\r

13

Powrót do początku wiersza bez przechodzenia do nowego wiersza.

\t

9

\”

34

Znak cudzysłowu. Normalnie zapisany znak cudzysłowu byłby interpretowany przez kompilator języka C++ jako koniec tekstu, jeżeli zatem potrzebne jest umieszczenie w tekście tego znaku, należy poprzedzić go ukośnikiem.

\’

39

Apostrof. Z identycznego powodu (znaki apostrofu otaczają pojedyncze znaki tekstu) konieczne jest poprzedzanie go ukośnikiem, jeżeli ma się pojawić w tekście.

\\

92

Wsteczny ukośnik. Ponieważ sam z siebie jest znakiem specjalnym, poprzedzającym inne znaki w kombinacjach, uzyskanie pojedynczego wstecznego ukośnika wymaga powtórzenia go.

Znak tabulacji. Przeskok do kolejnej, odgórnie ustalonej przez bibliotekę standardową kompilatora C lub C++ pozycji tabulacji na ekranie. Kombinacja przydatna w czasie tworzenia na ekranie tabel lub list.

Skoro jednak podprogram printf() daje aż takie możliwości kształtowania formy wydruku, nie ma sensu marnować cykli mikroprocesora na trzykrotne wywoływanie go z trzema różnymi parametrami. Podział wierszy można uzyskać w ramach jednego łańcucha znaków, umieszczając odpowiednią liczbę znaków specjalnych \n w tych miejscach tekstu, w których ma nastąpić przejście do nowego wiersza. Dzięki temu trzy osobne wyrażenia: printf("Ten tekst zaraz zostanie wyswietlony na ekranie!\n"); printf("Teraz drugi wiersz\n"); printf("A teraz trzeci!\n");

można zastąpić jednym (tylko dla wygody podzielonym na dwa wiersze tekstu źródłowego): printf("Ten tekst zaraz zostanie wyswietlony na ekranie\n" "Teraz drugi wiersz\nA teraz trzeci!\n");

Te dwa wiersze tekstu źródłowego składają się cały czas na jedno wyrażenie, rozpoczynające się nazwą podprogramu (printf), a kończące się średnikiem. Ponieważ parametry tekstowe funkcji nie mogą tak po prostu urywać się w jednym wierszu i ponownie

5

Znak, do którego następuje cofnięcie, nie jest kasowany z ekranu, jednak może zostać nadpisany kolejnymi wypisywanymi znakami.

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

61

Rysunek 2.4. Tekst wypisywany w wielu wierszach ekranu

rozpoczynać w następnym, w każdym wierszu należy zamknąć tekst znakiem cudzysłowu, by ponownie otworzyć go w ten sam sposób w następnym. Oczywiście, znaki cudzysłowu nie mają najmniejszego wpływu na to, gdzie wystąpi dzielenie wierszy na ekranie — to zależy wyłącznie od kombinacji \n. Warto jednak po każdej kombinacji \n przechodzić w tekście źródłowym programu do kolejnego wiersza, by układ tekstu źródłowego odpowiadał projektowanemu układowi tekstu drukowanego przez program. Oszczędza się w ten sposób czas i nie trzeba co chwilę kompilować i uruchamiać swojego programu.

Komentarze Komentarze stanowią element języka niemający wpływu na działanie programu, są jednak jednym z istotniejszych składników jego tekstu źródłowego. Komentarz to dowolny tekst umieszczony przez programistę w tekście źródłowym programu, mający na celu wyłącznie opisywanie programu. W czasie kompilacji komentarze są ignorowane: nie pokażą się na ekranie, nie są zapisane w wykonywalnej binarnej wersji programu, nie zajmują pamięci ani nie spowalniają działania gotowego programu. O ich mocy i znaczeniu można się dowiedzieć jedynie w momencie powrotu — po tygodniu, miesiącu lub roku od zakończenia lub przerwania prac — do tekstu źródłowego programu w celu usunięcia błędów lub dodania nowych funkcji. Po takiej przerwie

Część II  Język C

62

nie pamięta się już, jakie jest znaczenie każdego z wierszy programu, i jeżeli programista nie opatrzył ich przezornie czytelnymi komentarzami, czeka go mozolne przeglądanie tekstu źródłowego i rozszyfrowywanie sposobu działania programu6. Język C dopuszcza dwa rodzaje komentarza. Pierwszy typ jest bardzo prosty: wystarczy, że w dowolnym miejscu programu umieści się dwa ukośniki //, a wszystko, co będzie się znajdowało za nimi (aż do końca tego konkretnego wiersza tekstu źródłowego), zostanie potraktowane jako komentarz i zignorowane przez kompilator7: int main() // Podprogram główny, tutaj rozpoczyna się realizacja programu.

W komentarzach można stosować polskie znaki diakrytyczne i dowolne symbole.

Drugi typ komentarza pozwala ogarnąć swoim zasięgiem większe partie tekstu. Początek takiego komentarza oznacza się znakami /* (ukośnik i gwiazdka), a koniec — znakami */ (gwiazdka i ukośnik). Całość tekstu źródłowego znajdującego się między tymi dwiema sekwencjami znaków jest ignorowana przez kompilator: /* To jest komentarz zajmujący dwa wiersze tekstu. */

Komentarza tego typu używa się często do chwilowego wyłączania z działania fragmentów programu. Aby sprawdzić, jak program będzie się zachowywał bez kilku wierszy tekstu źródłowego lub bez całego bloku instrukcji, należy otoczyć go znakami początku i końca komentarza: /* printf("Ta instrukcja nigdy nie zostanie wykonana!\n"); */

Komentarzy można też używać do tworzenia ramek i szlaczków oddzielających większe fragmenty programu lub grupy instrukcji. Często w ramach komentarza tworzy się nawet semigraficzne8 ramki zawierające informacje o danym zbiorze źródłowym: // *** Wydruk wyników obliczeń ********************************** /******************************************** * Program obliczający obwód koła * * Wersja 1.0 z dnia 2013-01-10 * ****************************************** */

6

7

8

Oczywiście, każdy programista zna swój własny styl i patrząc na konkretne fragmenty tekstu źródłowego, szybko zorientuje się, jaką rolę odgrywają poszczególne wiersze. Mimo to czasem zdarza się kod tak zawiły (szczególnie gdy został zoptymalizowany pod kątem wydajności i stracił na czytelności), że konieczne jest poświęcenie dłuższej chwili na jego zrozumienie. W takim przypadku czytelne komentarze mogą znacząco pomóc. Ten typ komentarza wywodzi się z języka C++ i został wprowadzony do standardu języka C dopiero w 1999 r. Semigrafika to uproszczona grafika tworzona za pomocą znaków, w szczególności: podzbioru znaków dostępnych bezpośrednio z klawiatury komputera. Skrajną formą wykorzystania semigrafiki jest tzw. ASCII-art — sztuka tworzenia rozbudowanych obrazów i animacji (czarno-białych lub kolorowych) z wykorzystaniem wyłącznie semigrafiki.

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

63

Przechowywanie danych w pamięci Uruchamiane dotychczas programy niewątpliwie działają, wyświetlając w oknie efekt swojej pracy. Aby jednak program mógł być użyteczny, musi pobierać dane z jakiegoś urządzenia zewnętrznego9, poddawać je obróbce i wyświetlać wyniki obliczeń. Wypisywanie stałego tekstu ogranicza się w takim przypadku do wyświetlania komunikatów programu. Aby wprowadzać dane z klawiatury i móc je później przetwarzać lub wypisywać na ekranie, potrzebne jest tymczasowe przechowanie informacji w pamięci operacyjnej. W językach C oraz C++ służą do tego celu zmienne. Zmienna to fragment pamięci operacyjnej, któremu programista nadaje nazwę oraz deklaruje typ danych, które będą tam zapisane. Dzięki temu przy pisaniu programu w ogóle nie trzeba się troszczyć o to, w którym miejscu pamięci umieścić informacje i ile pamięci zarezerwować — wystarczy określić typ zmiennej i nadać jej nazwę, a kompilator wygeneruje kod maszynowy rezerwujący minimalną potrzebną ilość miejsca w pamięci i będzie się poprawnie odwoływał do informacji zapisanych w pamięci wszędzie tam, gdzie będzie to określone w tekście źródłowym programu.

Literały Opisywanie zmiennych warto zacząć od ich przeciwieństwa — literałów. Literał to wartość stała zapisana wprost w tekście źródłowym programu. Najważniejsze rodzaje literałów występujące w językach C i C++ zostały przedstawione w tabeli 2.2. Niektóre nazwy pojawiające się w tabeli mogą być niezrozumiałe, gdyż wymagają znajomości typów danych występujących w językach C i C++. Ewentualne wątpliwości powinny zostać jednak rozwiane w kilku kolejnych punktach tego rozdziału. Dodatki literowe u, l i f dopisywane do literałów liczbowych w celu zmiany typu danych mogą być zapisywane małymi lub wielkimi literami. Zazwyczaj przyjmuje się zapis małymi literami.

Deklaracja a definicja W całej książce będą się powtarzać dwa związane ze sobą terminy: deklaracja i definicja. Deklaracja jakiegoś elementu to informacja, że ten element występuje gdzieś w dalszej części programu i ma konkretne cechy, przedstawione w ramach tej deklaracji, jednak nie jest jeszcze znana treść (lub też zawartość) tego elementu. Definicja z kolei tworzy wcześniej zadeklarowany element i określa jego zawartość lub treść. Jeżeli element nie został wcześniej zadeklarowany, definicja od razu stanowi również jego deklarację. 9

Na przykład z klawiatury.

Część II  Język C

64 Tabela 2.2. Podstawowe literały w językach C i C++ Rodzaj literału Liczby całkowite dziesiętne

Przykłady zapisu

Dziesiętna wartość liczbowa

5

5

-14

-14

Liczby całkowite dziesiętne typu long

5l

5

-14l

-14

Liczby całkowite nieujemne dziesiętne

12u

12

1023u

1023

Liczby całkowite nieujemne dziesiętne typu long Liczby całkowite nieujemne ósemkowe Liczby całkowite nieujemne szesnastkowe

5ul

5

14ul

14

023

19

0771

505

0xA0

160

0x35

53

'A'

Znaki

Liczby zmiennoprzecinkowe typu float

.0f 1.0f -1.234f 1.5e-7f

Liczby zmiennoprzecinkowe typu double

.0

Liczby zmiennoprzecinkowe typu long double

.0l

1.0 -1.234 1.5e-7 1.0l -1.234l 1.5e-7l

Napisy

"ABC"

Uwagi Normalny zapis dziesiętny Zapis z literą l na końcu wartości dziesiętnej10. Dopuszczalne wyłącznie liczby dodatnie oraz zero. Zapis z literą u na końcu wartości dziesiętnej. Dopuszczalne wyłącznie liczby dodatnie oraz zero. Zapis z literami ul na końcu wartości dziesiętnej. Zapis ósemkowy liczby rozpoczyna się znakiem zera. Zapis szesnastkowy liczby rozpoczyna się znakami 0x.

65

Pojedynczy znak zapisany pomiędzy znakami apostrofu. Reprezentacją liczbową jest kod znaku według normy ASCII.

0

Zapis z literą f na końcu wartości dziesiętnej. Wymagana kropka dziesiętna i co najmniej jedna cyfra po kropce.

1 -1.234 1.5×10-7 0 1

Wymagana kropka dziesiętna i co najmniej jedna cyfra po kropce.

-1.234 1.5×10-7 0 1 -1.234

Zapis z literą l na końcu wartości dziesiętnej. Wymagana kropka dziesiętna i co najmniej jedna cyfra po kropce.

1.5×10-7 65 66 67 0

Każda litera jest traktowana jako osobny element typu char.

Analogicznie w bibliotece: deklaracją książki jest wpis w katalogu, a definicją sam tom. Znalezienie deklaracji książki (wpisu katalogowego) pozwala poznać podstawowe informacje o niej (autor, tytuł i kilka innych detali), jednak nie jej treść. Dopiero sięgnięcie po definicję (tom) rozwiewa wszelkie wątpliwości i pozwala skorzystać z treści. Jeżeli jednak nie zadeklarujemy książki (a któż w domowej biblioteczce tworzy karty katalogowe 10

Należy uważać na możliwą pomyłkę z uwagi napodobieństwo cyfry 1 i litery l. Z tego powodu do programowania zaleca się używanie czcionek o wyraźnej różnicy 1/l i 0/O.

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

65

książek?), tylko od razu sięgniemy po definicję w postaci tomu, również możemy odczytać wszystkie informacje o niej. Jedyna różnica polega na tym, że o ile kartę katalogową (deklarację) możemy łatwo skopiować i rozdać dowolnym osobom, by je poinformować o fakcie posiadania przez nas tej książki (a taki jest sens istnienia deklaracji również w językach C i C++), o tyle skopiowanie książki powoduje powstanie oddzielnych, niezwiązanych ze sobą egzemplarzy, a zatem nie pozwala na skoordynowane korzystanie z jednego tomu11. Podsumowując: wiele identycznych deklaracji informuje o istnieniu w programie pewnego obiektu (w sensie języka programowania) stworzonego przez jedną definicję.

Definiowanie zmiennej Definicja zmiennej skutkuje zarezerwowaniem miejsca w pamięci operacyjnej komputera. Ilość rezerwowanego miejsca jest wystarczająca dla określonego w definicji typu danych. Definicja zmiennej jest pojedynczym wyrażeniem i składa się z dwóch elementów oddzielonych znakiem spacji:  typu zmiennej, która może się składać z jednego (lub więcej) oddzielonych

znakami spacji słów kluczowych języków C i C++;  nazwy zmiennej, która musi stanowić jedno słowo, nie może zawierać żadnych

znaków specjalnych poza znakiem podkreślenia (a więc zabronione są też polskie znaki diakrytyczne) i nie może rozpoczynać się od cyfry. Oczywiście — zgodnie z wywodem przedstawionym w powyższym punkcie — definicja zmiennej stanowi zarazem jej deklarację. Oddzielne deklarowanie zmiennych jest wymagane jedynie w przypadku tworzenia programów składających się z wielu odrębnych plików z tekstem źródłowym, kompilowanych osobno do odrębnych plików wynikowych. Taki scenariusz zostanie przedstawiony w rozdziale 9., poświęconym programowaniu wielomodułowemu.

Całkowite typy danych Pamięć operacyjna komputera przechowuje sekwencje bitów, które można interpretować na różne sposoby. Najprościej jest rozumieć te sekwencje bitów jako liczby12 całkowite lub naturalne13. Oczywiście, ze względu na ograniczenia fizyczne nie jest możliwe przedstawienie nieskończonego przedziału liczb. W zależności od tego, ile miejsca w pamięci zostanie zarezerwowane na liczbę, dopuszczalny zakres liczbowy może się zmieniać.

11

Pomijając oczywiście kwestię prawa autorskiego, w świetle którego taka operacja jest nielegalna. Wyjaśnienie przekształcenia liczb binarnych (sekwencji bitów) na liczby całkowite i naturalne można znaleźć w Wikipedii pod adresami: http://pl.wikipedia.org/wiki/Dwójkowy_system_liczbowy, http://pl.wikipedia.org/wiki/Kod_uzupełnień_do_dwóch. 13 Dokładniej: sumy zbioru liczb naturalnych i zbioru jednoelementowego zawierającego liczbę zero. 12

Część II  Język C

66

Nazwy podstawowych typów danych dostępnych w językach C oraz C++ i reprezentujących typy liczbowe (całkowite i naturalne) przedstawione zostały — wraz z wyjaśnieniem — w tabeli 2.3. Tabela 2.3. Podstawowe typy danych języków C i C++ Nazwa typu14

Zakres wartości

Objaśnienie15

signed char

–128 … 127

Znak lub liczba zajmująca jeden bajt pamięci (8-bitowa) ze znakiem.

unsigned char

0 … 255

Znak lub liczba zajmująca jeden bajt pamięci (8-bitowa) bez znaku.

signed int

–2 147 483 648 … 2 147 483 647

Liczba 32-bitowa ze znakiem.

unsigned int

0 … 4 294 967 295

Liczba 32-bitowa bez znaku.

signed short [int]

–32 768 … 32 767

Liczba 16-bitowa ze znakiem

unsigned short [int]

0 … 65 535

Liczba 16-bitowa bez znaku.

signed long [int]

–2 147 483 648 … 2 147 483 647

Liczba 32-bitowa ze znakiem.

unsigned long [int]

0 … 4 294 967 295

Liczba 32-bitowa bez znaku.

63

63

signed long long [int]

–2 … 2 –1

Liczba 64-bitowa ze znakiem.

unsigned long long [int]

0 … 264–1

Liczba 64-bitowa bez znaku.

Najważniejsza uwaga dotyczy rozmiarów obszarów pamięci zajmowanych przez zmienne poszczególnych typów oraz — co z tego wynika — zakresów liczb mieszczących się w zmiennej danego typu. Programista może być pewien jedynie tego, że obydwa typy char (ze znakiem oraz bez znaku) odpowiadają najmniejszej jednostce pamięci komputera — w przypadku komputerów osobistych klasy PC16 będzie to jeden bajt. Rozmiary zmiennych dalszych typów (int, short int, long int, long long int) nie są ściśle zdefiniowane: podane dane dotyczą kompilatora Microsoft Visual Studio działającego na komputerze PC i tworzącego oprogramowanie 32- oraz 64-bitowe, jednak w przypadku innej architektury komputera lub innego kompilatora mogą być inne. Jedyna żelazna zasada określa, że zmienna typu short int musi mieć rozmiar (i zakres) co najmniej dorównujący zmiennym typu char, zmienna typu int — zmiennej typu short int itd. Rozmiary typów zmiennych obsługiwanych przez kompilator działający na konkretnym komputerze można łatwo określić za pomocą własnego programu. Zostanie to pokazane w dalszej części tego rozdziału.

Warto zauważyć, że każdy z wymienionych typów zmiennych występuje w dwóch wersjach: signed (ze znakiem) oraz unsigned (bez znaku). Ten drugi umożliwia przechowywanie jedynie liczb całkowitych nieujemnych. Ograniczenie obsługiwanego zakresu 14

Elementy podane w nawiasach kwadratowych są opcjonalne. Typowe. Rozmiar danych oraz dopuszczalny zakres wartości zależą od kompilatora i architektury sprzętowej. 16 W całej książce wyrażenie „komputer klasy PC” oznacza komputery wywodzące się (i zgodne) z konstrukcji IBM PC. 15

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

67

do liczb nieujemnych pozwala dwukrotnie zwiększyć liczbę możliwych do przedstawienia liczb dodatnich, niesie ze sobą jednak pewne ryzyko. Jeżeli np. zmienną przechowującą liczbę sztuk towaru znajdującego się na stanie zdefiniuje się (całkowicie poprawnie i zgodnie z logiką) jako unsigned long i na skutek jakiegoś przeoczenia zmniejszy się stan magazynowy z zera o jedną sztukę, zamiast liczby –1, która pojawiłaby się w przypadku użycia typu danych signed long, stan magazynu będzie wynosił… 4 294 967 295 sztuk towaru! Języki C i C++ nigdy nie sprawdzają, czy wynik operacji arytmetycznej wykonywanej przez program mieści się w zakresie wartości typu danych zmiennej, do której ma trafić — jest to czasem wielka zaleta, wymaga jednak od programisty bardzo daleko idącej ostrożności i uwagi17. W powyższym akapicie przy typach zmiennych signed long i unsigned long pominięte zostało słowo int, znajdujące się w tabeli 2.3 w nawiasach kwadratowych. W przypadku typów zmiennych zawierających słowa long oraz short używanie słowa kluczowego int jest opcjonalne — można go użyć, jednak jest to zupełnie niepotrzebne. Tak samo nie trzeba za każdym razem określać, czy tworzona zmienna ma być typu całkowitego (ze znakiem) czy naturalnego (bez znaku): jeżeli pominie się słowa kluczowe signed lub unsigned, kompilator przyjmie domyślnie, że chodzi o liczbę całkowitą (ze znakiem, typ signed)18. Oto przykładowa definicja trzech zmiennych liczbowych, każdej innego typu: char Literka; unsigned long LiczbaCiastek; int i;

W przypadku definiowania kilku zmiennych tego samego typu, nie trzeba dla każdej z nich tworzyć nowej definicji. Wpisując jednokrotnie typ danych, a następnie wielokrotnie nazwy zmiennych (rozdzielone przecinkiem), można utworzyć w ramach jednego wyrażenia wiele zmiennych: unsigned long LiczbaCiastek, LiczbaTortow, LiczbaCukierkow;

Wyrażenia definiujące zmienne można też rozszerzyć o literał reprezentujący wartość początkową. Dzięki temu już w czasie uruchamiania programu w odpowiednie komórki pamięci (przypisane tym zmiennym) wpisane zostaną określone liczby: unsigned long LiczbaCiastek = 0ul, LiczbaTortow = 5ul, LiczbaCukierkow = 100ul; signed int i = -5;

17

Z tego właśnie powodu twórcy języka Java zlikwidowali możliwość korzystania z typów liczbowych bez znaku. Choć ogranicza to możliwość wystąpienia błędu spowodowanego odjęciem liczby od zera, nie likwiduje ryzyka przepełnienia liczby i przeskoczenia bezpośrednio od liczb ujemnych do dodatnich lub w drugą stronę. Z dwojga złego lepsza jest elastyczność języków C i C++ (którą można obudować mechanizmami zabezpieczającymi przed takimi błędami) niż pseudozabezpieczenia tego typu. 18 Możliwa jest zmiana domyślnego typu zmiennych całkowitych na unsigned za pomocą opcji kompilatora, jednak nie jest to zalecane. Ogólnie mówiąc, w każdym przypadku, gdy zakres przedstawianych przez zmienną liczb jest istotny, należy wprost podać słowo kluczowe signed lub unsigned.

Część II  Język C

68

Jeżeli zdefiniuje się zmienną, nie nadając jej wartości początkowej, wartością zmiennej będzie całkowicie przypadkowa liczba. Z tego powodu pierwsze użycie zmiennej musi polegać na zapisaniu do tej zmiennej jakiejś sensownej wartości.

W języku C zmienne muszą być definiowane na początku bloku instrukcji, w którym są używane (czyli zaraz za nawiasem klamrowym { otwierającym blok). Kolejność definiowania zmiennych przekłada się na kolejność zapisywania informacji w pamięci operacyjnej, jednak w języku C nie ma to wpływu na działanie programu19. Raz zdefiniowana zmienna istnieje w pamięci aż do momentu, gdy blok instrukcji zostanie opuszczony przez dotarcie z realizacją kodu do nawiasu klamrowego zamykającego blok lub przez wykonanie instrukcji powodującej natychmiastowe wyjście z bloku (np. return). Warto też pamiętać, że zmienna zdefiniowana w bloku instrukcji jest dostępna („widoczna”) wyłącznie w tym bloku oraz w blokach podrzędnych wobec niego (zagnieżdżonych w nim). O ile zatem poniższy fragment programu jest poprawny: { int a = 7; { a = 12; } }

ponieważ zmienna a jest zdefiniowana w bloku nadrzędnym i następnie zmodyfikowana w bloku podrzędnym, o tyle poniższy fragment kodu spowoduje pojawienie się błędu kompilacji: { { int a = 7; } a = 12; }

gdyż w momencie zakończenia realizacji bloku podrzędnego pamięć przydzielona zmiennej a zostanie zwolniona, a nazwa przestanie obowiązywać.

Znakowe typy danych Zmienne typu char przewidziane są do przechowywania znaków należących do zbioru ASCII. Oczywiście, znaki są przechowywane w pamięci w postaci ich kodów liczbowych i nic nie stoi na przeszkodzie, aby traktować zmienne typu char jako zwykłe zmienne liczbowe. Jeżeli jednak faktycznie chce się zapisać w pamięci pojedynczą

19

Kolejność definiowania zmiennych może mieć co najwyżej niewielki wpływ na wydajność działania programu. Stanowi to kontrast do języka C++, w którym kolejność definiowania zmiennych ma podstawowe znaczenie.

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

69

literę20, nie trzeba wpisywać jako wartości początkowej kodu znaku odczytanego z tablic. Czynność zamiany litery na odpowiadający jej kod należy zrzucić na kompilator, umieszczając znak w pojedynczych znakach cudzysłowu: char Litera = 'a';

Zmienne mające przechowywać znaki tekstowe definiuje się jako char, nie określając, czy mają być one traktowane jako liczby ze znakiem, czy bez znaku. Należy jednak pamiętać, że — zgodnie z tym, co zostało napisane w poprzednim punkcie na temat domyślnej odmiany zmiennych całkowitych — najczęściej kompilator przyjmie domyślnie typ signed char.

Zawsze należy pamiętać, że zmienne znakowe to tak naprawdę zmienne całkowite przechowujące liczbę stanowiącą kod znaku. W związku z tym możliwe jest wykonywanie na zmiennych znakowych normalnych operacji arytmetycznych. Na przykład litery A i C mają odpowiednio kody 65 i 67. Wynika z tego, że dodanie liczby 2 do zmiennej znakowej przechowującej literę A spowoduje, że jej zawartość zmieni się z A... na C.

Zmiennoprzecinkowe typy danych Dotychczas przedstawione typy zmiennych pozwalały operować na liczbach całkowitych oraz na znakach, które również można przedstawić w postaci liczby naturalnej (kodu znaku). Czasem jednak przydaje się możliwość wykonywania obliczeń na liczbach rzeczywistych, zawierających oprócz części całkowitej również część ułamkową. Ze względów technicznych komputery nie są w stanie operować na liczbach rzeczywistych z dowolną dokładnością. Liczby przechowywane w pamięci są zawsze zaokrąglane do najbliższej postaci możliwej do zapisania binarnie. W efekcie tylko kilka cyfr znaczących nie zmienia się i może się okazać, że po dodaniu liczb 10 000 000 i 0,000 000 001 wynikiem jest po prostu 10 000 000. Zbiór tak zaokrąglanych liczb rzeczywistych nazywamy zbiorem liczb zmiennoprzecinkowych (ang. floating-point). Język C++ oferuje trzy typy zmiennych pozwalających przechowywać liczby zmiennoprzecinkowe. W przeciwieństwie do typów całkowitych nie ma tutaj podziału na odmiany dopuszczające (signed) i zabraniające (unsigned) używania liczb ujemnych: każda liczba zmiennoprzecinkowa może być ujemna:  typ float stanowi podstawowy typ zmiennoprzecinkowy, wystarczający

do większości zastosowań;  typ double pozwala na wykonywanie obliczeń z większą dokładnością

(kosztem większej zajętości pamięci operacyjnej); 20

W zmiennych typu char przechowywać można właśnie tylko pojedyncze litery. Nie da się w takiej zmiennej przechować kilku znaków (wyrazu, zdania, frazy). Tematyce zapisywania w pamięci operacyjnej fragmentów tekstu składających się z więcej niż jednego znaku poświęcone będą kolejne rozdziały książki.

Część II  Język C

70

 typ long double pozwala jeszcze bardziej zwiększyć dokładność, jeszcze

bardziej jednak zwiększając objętość pamięci zajmowanej przez każdą zmienną tego typu. Wszystkie zasady dotyczące definiowania zmiennych oraz nadawania im wartości początkowych są w przypadku zmiennych typu zmiennoprzecinkowego identyczne jak dla zmiennych całkowitych. Należy jedynie pamiętać, że literały opisujące początkową zawartość zmiennej muszą zawierać kropkę i część dziesiętną oraz dodatek literowy właściwy dla wybranego typu zmiennej: float Srednica = 6.0f; double Obwod = 2.0 * 3.1415 * Srednica;

W językach C i C++ — w przeciwieństwie do zasad obowiązujących w języku polskim — znakiem oddzielającym część całkowitą od ułamkowej jest zawsze kropka. Co gorsza, zastąpienie kropki przecinkiem powoduje często powstanie programu poprawnego składniowo, lecz działającego niepoprawnie, ponieważ przecinek — jak zostało pokazane w przykładach definicji kilku zmiennych tego samego typu — rozdziela różne wyrażenia.

Nazywanie zmiennych Nazwa zmiennej musi spełniać pewne wspomniane już wcześniej wymogi techniczne. Aby jednak w trakcie pisania programu nie mieć wątpliwości, w jakim celu zostały stworzone poszczególne zmienne, warto przestrzegać paru zasad.  Nazwy zmiennych nie powinny być zbyt długie, by nie były do siebie zbyt

podobne i by prosto wprowadzało się je z klawiatury. Nazwa zmiennej LiczbaOpakowanZawierajacychLandrynkiCzekoladowe nie dość, że może spowodować kłopoty z kompilacją lub działaniem programu, to jest całkowicie nieużyteczna — lepiej ją skrócić21, a definicję zmiennej opatrzyć odpowiednim komentarzem wyjaśniającym przeznaczenie zmiennej.  Nazwy zmiennych muszą być tak długie, by opisywały jasno znaczenie

zmiennych i nie używały zbyt daleko idących skrótów. Wspomniana przed chwilą zmienna o nazwie LiczbaOpakowanZawierajacychLandrynkiCzekoladowe po nazwaniu skrótem LOZLC lub LiOpZaLaCz będzie całkowicie niezrozumiała.  Nie należy wykorzystywać wielokrotnie jednej zmiennej do różnych celów.

Zdefiniowanie kolejnej zmiennej nie spowalnia znacząco działania programu, a może pomóc zapobiec błędom wynikającym z modyfikowania wartości zmiennej w czasie, gdy jest ona wykorzystywana do innych celów.  W wielu przypadkach tymczasowe zmienne służące do zliczania elementów lub numeru kolejnego przebiegu pętli nazywa się x, y, i, j, k. Nie ma nic złego

w stosowaniu takich nazw w krótkich fragmentach programu i w wymienionych

21

Na przykład do postaci: nOpakLandrCzek.

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

71

zastosowaniach, nie wolno jednak stosować tej konwencji w celu trwałego przechowywania informacji. Więcej na temat stosowania tak nazwanych zmiennych znajduje się w rozdziale 5. Należy pamiętać, że języki C i C++ zawsze rozróżniają małe i wielkie litery. Każde odwołanie się do zmiennej musi następować poprzez nazwę identyczną co do znaku z definicją tej zmiennej — dla kompilatora nazwy cena, Cena i CENA to trzy całkowicie różne symbole.

Deklarowanie własnych typów danych Programista może chcieć nazwać wybrany typ danych własną nazwą tak, by odwoływać się do długiej nazwy (np. unsigned short int) za pomocą krótkiego identyfikatora (np. ushort). Ma to szczególne znaczenie w przypadku złożonych typów danych, opisanych w dalszych rozdziałach książki. Język C daje możliwość deklarowania własnych typów danych na podstawie już istniejących (standardowych lub własnych) deklaracji. Służy do tego słowo kluczowe typedef. Pierwszym elementem występującym po typedef jest istniejące określenie typu danych, po którym następuje nowa, własna nazwa typu danych. Na przykład deklaracja: typedef unsigned short int ushort;

tworzy wspomniany wyżej nowy typ danych ushort, będący całkowitym odpowiednikiem unsigned short int. Dzięki temu zamiast tworzyć zmienne jako: unsigned short int Zmienna;

można napisać: typedef unsigned short int ushort; ushort Zmienna;

Obliczenia Operacje algebraiczne Na zmiennych można wykonywać dowolne operacje arytmetyczne, korzystając z literałów (wartości stałych wprowadzonych w programie), wartości samych zmiennych oraz znaków22 podstawowych czterech operacji arytmetycznych: +, -, *, / oraz nawiasów okrągłych ( i ) (narzucających kolejność wykonywania operacji, jeżeli ma być ona niezgodna ze zwykłą kolejnością wykonywania działań arytmetycznych). Przypisanie zmiennej nowej wartości uzyskuje się, tworząc wyrażenie składające się z nazwy 22

Znak (symbol) odpowiadający pewnej operacji na liczbie nazywa się w języku C operatorem.

Część II  Język C

72

zmiennej, znaku równości oraz sekwencji operacji, zaś odwołanie się do wartości już istniejącej zmiennej wymaga podania — zamiast konkretnej liczby — nazwy wybranej zmiennej. Wyrażenia algebraiczne mogą pojawiać się już w wyrażeniu deklarującym zmienną lub w dalszych wyrażeniach, narzucających zmiennej nową wartość: int Cena, LiczbaCiastek = 5; int LiczbaPaczekCiastek = LiczbaCiastek / 2; Cena = (LiczbaCiastek + 5) * LiczbaPaczekCiastek;

Takie same operacje można wykonywać też na znakach — w końcu zmienne typu char przechowują po prostu kody odpowiadające poszczególnym literom. Na przykład poniższe wyrażenie przypisze zmiennej Litera znak będący następnym po a: char Litera = 'a' + 1;

Biorąc pod uwagę, że litera a ma kod równy 97, zmienna Litera przybierze wartość 98, która odpowiada literze b. Należy pamiętać, że język C nie dysponuje mechanizmem weryfikującym, czy wynik operacji ma sens z punktu widzenia człowieka. Jeżeli zatem doda się jakąś wartość do kodu litery z, w efekcie uzyska się kod pewnego znaku graficznego leżącego w dalszej części tabeli kodów. Analogicznie dodanie wartości 10 do znaku o kodzie 255 spowoduje uzyskanie znaku o kodzie 9 w efekcie zawinięcia się numeracji 0...255.

Specjalną operacją arytmetyczną, mającą zastosowanie w przypadku liczb całkowitych, jest dzielenie modulo, czyli wyznaczanie reszty z dzielenia całkowitego. Operacji dzielenia modulo odpowiada operator %. Tabela 2.4 przedstawia wynik dzielenia całkowitego oraz dzielenia modulo dla dzielnika równego 3 i dzielnej zmieniającej się w zakresie od 0 do 10. Tabela 2.4. Wynik realizacji operacji dzielenia całkowitego i dzielenia modulo i

0

1

2

3

4

5

6

7

8

9

10

i / 3

0

0

0

1

1

1

2

2

2

3

3

i % 3

0

1

2

0

1

2

0

1

2

0

1

Dzielenie modulo wykorzystuje się przy numerowaniu pól lub zawijaniu liczb do pewnego określonego przedziału. Dzieląc jakąś liczbę całkowitą modulo x, możemy mieć pewność, że wynik będzie należał do zbioru liczb całkowitych z zakresu od 0 do x – 1. Warto zauważyć, że język C nie dysponuje operatorem potęgowania. Symbol ^, często nieświadomie wykorzystywany przez adeptów tego języka programowania, ma zupełnie inne znaczenie, wyjaśnione w dalszej części tego rozdziału. Potęgowanie można uzyskać albo przez wielokrotne mnożenie (szczególnie przy małej wartości wykładnika), albo pisząc własny podprogram, albo korzystając ze standardowego podprogramu pow()23. 23

Podprogram pow() wykorzystuje logarytmowanie, dzięki czemu wyznacza wartość liczby podniesionej do dowolnej potęgi, w tym o niecałkowitym lub ujemnym wykładniku. Jest to jednak okupione długim czasem realizacji obliczeń. Z tego powodu przy wyznaczaniu potęg o niskim, całkowitym wykładniku należy stosować wielokrotne mnożenie.

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

73

Operacje bitowe Dodatkową kategorią operacji, które można wykonywać na liczbach całkowitych, są operacje bitowe. Pozwalają one na zmienianie lub odczytywanie stanu pojedynczych bitów liczby. Oczywiście, aby z sensem operować na wartościach bitów, należy znać system dwójkowy oraz wagi poszczególnych bitów. Tabela 2.5 przedstawia dostępne operacje bitowe, odpowiadające im operatory, wyjaśnienie sposobu działania operacji oraz przykład zastosowania. Tabela 2.5. Operacje bitowe w języku C Operacja

Operator

Wyjaśnienie

Przykład

Negacja

~

Zamienia w danej liczbie wszystkie bity o wartości 0 na 1, a wszystkie bity o wartości 1 na 0.

unsigned char x = 128; unsigned char z = ~x;

Łączy dwie liczby, umieszczając w wyniku bit o wartości 1 na pozycjach, na których w choć jednej z liczb źródłowych również znajduje się jedynka.

unsigned char x = 6; unsigned char y = 3; unsigned char z = x | y;

Łączy dwie liczby, umieszczając w wyniku bit, tylko jeżeli w obydwu liczbach źródłowych bity na tej pozycji mają wartość 1.

unsigned char x = 6; unsigned char y = 3; unsigned char z = x & y;

NOT

Suma logiczna

|

OR

Iloczyn logiczny

&

AND

Suma modulo XOR

^

Łączy dwie liczby, umieszczając w wyniku bit równy 1, tylko jeżeli w liczbach źródłowych jest zapalony bit na tej pozycji jedynie w jednej z liczb.

(zmienna z przyjmuje w tym momencie wartość 127, ponieważ bit o wadze 128 zostaje zgaszony, a zapalają się bity o wagach 64, 32, 16, 8, 4, 2 i 1).

(zmienna z przyjmuje w tym momencie wartość 7, ponieważ w x zapalone są bity o wagach 4 i 2, a w y — o wagach 2 i 1, więc w wyniku zapalane są bity o wagach 4, 2 i 1).

(zmienna z przyjmuje w tym momencie wartość 2, ponieważ w x zapalone są bity o wagach 4 i 2, a w y — o wagach 2 i 1, więc w wyniku zapalany jest tylko wspólny dla nich bit o wadze 2). unsigned char x = 6; unsigned char y = 3; unsigned char z = x ^ y;

(zmienna z przyjmuje w tym momencie wartość 5, ponieważ w x zapalone są bity o wagach 4 i 2, a w y — o wagach 2 i 1, więc w wyniku zapalane są bity o wagach 4 i 1, niepowtarzające się w tych dwóch liczbach).

Część II  Język C

74 Tabela 2.5. Operacje bitowe w języku C — ciąg dalszy Operacja

Operator

Wyjaśnienie

Przykład

Przesunięcie bitowe w lewo

2;

SHL

Przesunięcie bitowe w prawo SHR

>>

(zmienna z przyjmuje w tym momencie wartość 24, ponieważ bity o wagach 4 i 2 przesuwają się w lewo o dwie pozycje i zyskują wagi 16 i 8).

(zmienna z przyjmuje w tym momencie wartość 1, ponieważ bity o wagach 4 i 2 przesuwają się w prawo o dwie pozycje, przy czym jeden zyskuje wagę 1, a drugi wysuwa się całkiem z liczby).

Operacje przesunięć bitowych odpowiadają — z pewnymi ograniczeniami — mnożeniu przez kolejne potęgi dwójki. Przesunięcie w lewo o jedną pozycję oznacza pomnożenie przez 2, zaś przesunięcie w prawo — podzielenie przez 2. Nie warto jednak zastępować operacji mnożenia i dzielenia przesunięciami. Tekst źródłowy programu powinien zawierać operacje mnożenia i dzielenia; kompilator w razie potrzeby sam dokona zamiany na odpowiadające im przesunięcia bitów.

Skracanie wyrażeń algebraicznych Wyrażenie algebraiczne obliczające nową wartość zmiennej może wykorzystywać w obliczeniach poprzednią wartość tej zmiennej. Na przykład poniższa instrukcja przypisania powoduje wpisanie do zmiennej miejsce wartości tej zmiennej pomniejszonej o jeden; jeżeli zmienna miejsce zawierała liczbę 5, po realizacji instrukcji zostanie do niej wpisana wartość 5 – 1 = 4: miejsce = miejsce - 1;

Operacje modyfikowania zawartości zmiennej (czyli nadawania jej nowej wartości wyznaczonej na bazie starej wartości) występują w programach tak często, że w języku C przeznaczono dla nich osobne operatory realizujące w jednym kroku operację matematyczną oraz przypisanie nowej wartości. Te operatory zostały przedstawione — wraz z objaśnieniem ich znaczenia — w tabeli 2.6. Tego typu skrótów warto używać zawsze, gdy dokonuje się modyfikacji wartości zmiennej za pomocą jednego typu operacji arytmetycznej24. Podany na początku tego punktu przykład można zapisać w następujący sposób: 24

Na przykład samego dodawania lub samego mnożenia. Należy przy tym pamiętać, że odejmowanie jest formą dodawania, a dzielenie — formą mnożenia.

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

75

Tabela 2.6. Operatory modyfikujące wartości zmiennych Operator

Znaczenie

Przykład wykorzystania

+=

Dodanie wartości do zmiennej.

-=

Odejmowanie wartości od zmiennej.

*=

Przemnożenie zmiennej przez wartość.

/=

Podzielenie zmiennej przez wartość.

pociag = pociag + 5; > pociag += 5; miejsce = miejsce – n + 1; > miejsce -= (miejsce – 1); punkty = punkty * 1.2; > punkty *= 1.2; cena = cena / 1.7; > cena /= 1.7;

miejsce -= 1;

Nie ma sensu próbować skracać wyrażeń używających różnego typu operacji arytmetycznych. Na przykład wyrażenie punkty = punkty * 2 + 1; po wprowadzeniu skrótów stanie się mniej czytelne niż w tej prostej postaci.

Ze wszystkich operacji modyfikowania zawartości zmiennej olbrzymią większość stanowią operacje odejmowania i dodawania jedynki. Dodatkowo wybór właściwej kolejności wprowadzania modyfikacji i zwracania wyniku pozwala w tym przypadku na daleko idącą optymalizację wynikowego kodu maszynowego. Z tego powodu w języku C wprowadzono cztery specjalistyczne operatory dokonujące wyłącznie zwiększania wartości zmiennej o jeden (tzw. inkrementowanie zmiennej) oraz zmniejszenia wartości zmiennej o jeden (tzw. dekrementowanie zmiennej). Pełną ich listę przedstawiono — wraz z dokładnym objaśnieniem — w tabeli 2.7. Tabela 2.7. Operatory inkrementacji i dekrementacji Operator

Nazwa

Opis

++zmienna

preinkrementacja

Wartość zmiennej zmienna zwiększana jest o jeden. Wartość wyrażenia jest równa zmienionej wartości zmiennej.

zmienna++

postinkrementacja

Wartość zmiennej zmienna zwiększana jest o jeden. Wartość wyrażenia jest równa poprzedniej wartości zmiennej.

--zmienna

predekrementacja

Wartość zmiennej zmienna zmniejszana jest o jeden. Wartość wyrażenia jest równa zmienionej wartości zmiennej.

zmienna--

postdekrementacja

Wartość zmiennej zmienna zmniejszana jest o jeden. Wartość wyrażenia jest równa poprzedniej wartości zmiennej.

Przykładowe wyrażenie przedstawione na początku tego podpunktu można zatem skrócić jeszcze bardziej, do następującej postaci: --miejsce;

O ile sama idea zwiększania i zmniejszania wartości zmiennej o jeden jest prosta, o tyle mogą pojawić się wątpliwości co do stwierdzenia „wartość wyrażenia równa jest zmienionej (lub poprzedniej) wartości zmiennej”. W językach C i C++ każde wyrażenie

Część II  Język C

76

podstawienia nowej wartości również ma wartość (równą tej podstawianej) i może być wykorzystane do tworzenia dalszych wyrażeń algebraicznych. Na przykład fragment programu: int a = b = c =

a, b, c; 5; 6; a + b + 7;

można zapisać w następujący sposób: int a, b, c; c = (a = 5) + (b = 6) + 7;

bowiem wartość wyrażenia (a = 5) wynosi po prostu 5. Takie wykorzystanie operacji podstawienia nowej wartości zmiennej nie zmienia oczywiście niczego w samym podstawieniu: wyrażenie (a = 5) w pierwszej kolejności przypisze zmiennej a nową wartość 5, a następnie tę wartość zwróci do wykorzystania nadrzędnemu wyrażeniu algebraicznemu. Program przykładowy zaprezentowany na listingu 2.6 prezentuje sposób działania wszystkich czterech operatorów związanych z inkrementacją i dekrementacją zmiennych. W programie tym wyrażenia zmieniające wartości zmiennych zostały podane od razu jako parametry funkcji printf(), w pierwszej kolejności modyfikując zmienną, a dopiero potem przekazując wartość jako parametr funkcji. Rysunek 2.5 pokazuje, że mimo wykonania operacji dodania (lub odjęcia) jedynki na dwa sposoby wartość zmiennej jest oczywiście taka sama, w czasie wykonywania operacji zwracana wartość wyrażenia różni się: operacje z przedrostkiem pre- najpierw zmieniają wartość zmiennej, a następnie zwracają nową, podczas gdy operacje z przedrostkiem post- zaczynają od zwrócenia aktualnej wartości zmiennej, a dopiero potem ją modyfikują. Listing 2.6. Porównanie efektów działania post- i preinkrementacji (dekrementacji) #include #include // Tylko Visual Studio int main() { int a = 5, b = 5, c = 5, d = 5; printf("*** Inkrementacja ***\n" "Przed: a = %i, b = %i\n", a, b); printf("++a (preinkrementacja): %i\n", ++a); printf("b++ (postinkrementacja): %i\n", b++); printf("Po: a = %i, b = %i\n\n", a, b); printf("*** Dekrementacja ***\n" "Przed: c = %i, d = %i\n", c, d); printf("--c (predekrementacja): %i\n", --c); printf("d-- (postdekrementacja): %i\n", d--); printf("Po: c = %i, d = %i\n", c, d); _getch(); // Tylko Visual Studio return 0; }

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

77

Rysunek 2.5. Efekt działania programu przykładowego z listingu 2.6

W większości przypadków poprzednia wartość zmiennej nie jest potrzebna do działania programu. Ponadto jej przechowywanie, niezależnie od wprowadzanej modyfikacji, powoduje, że zastosowanie postinkrementacji i postdekrementacji zazwyczaj skutkuje wygenerowaniem bardziej obszernego, wolniejszego kodu maszynowego. Poza specyficznymi przypadkami, w których zastosowanie postinkrementacji i postdekrementacji jest uzasadnione, należy w instrukcjach typu: zmienna = zmienna + 1;

wykorzystywać operatory preinkrementacji lub predekrementacji: ++zmienna;

Wyprowadzanie danych W poprzednich punktach zostało wyjaśnione zagadnienie definiowania zmiennych (a więc: rezerwowania pamięci na zapisywane w nich dane, określania typu ich danych i nadawania im nazwy), zapisywania danych do zmiennych oraz przeprowadzania operacji arytmetycznych i bitowych na liczbach zapisanych w zmiennych. Do tworzenia funkcjonalnych programów, niezbędna jest jeszcze możliwość wypisania zawartości wybranych zmiennych na ekranie wraz z dowolnym opisem. Z pomocą przychodzi tu po raz kolejny podprogram printf(). Jego niesamowita siła polega na tym, że tekst podawany jako parametr nie jest po prostu wyprowadzany bez zmian na ekran komputera, lecz przekształcany z uwzględnieniem zawartych w nim znaków specjalnych. Wcześniej już zostało pokazane zastosowanie kodów specjalnych rozpoczynających się od znaku \, jednak podprogram printf() może też interpretować specjalne kombinacje znaków rozpoczynające się znakiem % i wstawiać wartości zmiennych w tak zarezerwowane w tekście pola. Najczęściej używane specjalne kombinacje kodów zaczynających się znakiem % zostały przedstawione w tabeli 2.8. Aby użyć wybranego kodu z powyższej tabeli i zastąpić go w wyświetlanym tekście wartością wybranej zmiennej, należy umieścić ten kod w tekście podawanym jako pierwszy parametr podprogramu printf(), a następnie zaraz za tekstem, po przecinku, podać nazwę zmiennej, której wartość ma być wstawiona zamiast tego kodu formatującego: int Zmienna = 20; printf("A oto wartosc zmiennej: %i\n", Zmienna);

Część II  Język C

78

Tabela 2.8. Kody formatujące wartości zmiennych i przygotowujące je do wyświetlenia na ekranie za pomocą podprogramu printf() Kod

Zastępowany przez zmienne typu

%c

char

W miejscu występowania kodu wyświetlany jest znak o kodzie zapisanym w podanej zmiennej.

%hhi %hhd

signed char

W miejscu występowania kodu wyświetlana jest liczba dziesiętna zapisana w podanej zmiennej.

%hhu

unsigned char

W miejscu występowania kodu wyświetlana jest liczba dziesiętna zapisana w podanej zmiennej.

%i

signed int

W miejscu występowania kodu wyświetlana jest liczba dziesiętna zapisana w podanej zmiennej.

%u

unsigned int

W miejscu występowania kodu wyświetlana jest liczba dziesiętna zapisana w podanej zmiennej.

%hi %hd

signed short int

W miejscu występowania kodu wyświetlana jest liczba dziesiętna zapisana w podanej zmiennej.

%hu

unsigned short int

W miejscu występowania kodu wyświetlana jest liczba dziesiętna zapisana w podanej zmiennej.

%li %ld

signed long int

W miejscu występowania kodu wyświetlana jest liczba dziesiętna zapisana w podanej zmiennej.

%lu

unsigned long int

W miejscu występowania kodu wyświetlana jest liczba dziesiętna zapisana w podanej zmiennej.

%Li %Ld

signed long long int

W miejscu występowania kodu wyświetlana jest liczba dziesiętna zapisana w podanej zmiennej.

%Lu

unsigned long long int

W miejscu występowania kodu wyświetlana jest liczba dziesiętna zapisana w podanej zmiennej.

%f

float

W miejscu występowania kodu wyświetlana jest liczba zmiennoprzecinkowa zapisana w podanej zmiennej.

%lf

double

W miejscu występowania kodu wyświetlana jest liczba zmiennoprzecinkowa zapisana w podanej zmiennej.

%Lf

long double

W miejscu występowania kodu wyświetlana jest liczba zmiennoprzecinkowa zapisana w podanej zmiennej.

%x

int

W miejscu występowania kodu wyświetlana jest liczba całkowita zapisana w podanej zmiennej, przekształcona do postaci szesnastkowej.

%lx %lX

long int

W miejscu występowania kodu wyświetlana jest liczba całkowita zapisana w podanej zmiennej, przekształcona do postaci szesnastkowej.

%Lx %LX

long long int

W miejscu występowania kodu wyświetlana jest liczba całkowita zapisana w podanej zmiennej, przekształcona do postaci szesnastkowej.

%p

*

W miejscu występowania kodu wyświetlany jest adres komórki pamięci zapisany w zmiennej wskaźnikowej25.

%d

%X

%%

25

Uwagi

Kod zastępowany pojedynczym znakiem % (ponieważ sam znak % jest znakiem początkowym kodów i ponieważ występuje w pojedynkę, nigdy nie jest wyświetlany na ekranie).

Zmienne wskaźnikowe omówione są w dalszej części książki.

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

79

Tą samą metodą można za pomocą jednego wywołania podprogramu printf() spowodować wyprowadzenie na ekran wartości kilku zmiennych. Wystarczy umieścić w tekście kilka kodów formatujących rozpoczynających się znakiem %, a za tekstem podać (w tej samej kolejności) kilka nazw zmiennych, rozdzielonych przecinkami. Najważniejsze jest, by liczba kodów formatujących oraz podanych nazw zmiennych była identyczna. Można nawet wartość tej samej zmiennej wyprowadzić na ekran kilkakrotnie, za każdym razem stosując inny kod formatujący: unsigned int calkowita = 123u; float zmiennoprzecinkowa = 345.67f; char znak = 'K'; printf("1:%d 2:%x 3:%f 4:%c 5:%d 6:%x 7:%X\n", calkowita, calkowita, zmiennoprzecinkowa, znak, znak, znak, znak);

Jako parametry podprogramu printf() można też podać całe wyrażenia algebraiczne, bez konieczności wcześniejszego zapisywania ich wyników w osobnych zmiennych: float Mnozna = 12.0f, Mnoznik = 13.5f; printf("%f x %f = %f\n", Mnozna, Mnoznik, Mnozna * Mnoznik);

Na listingu 2.7 znajdują się powyższe przykłady połączone w kompletny program możliwy do skompilowania i uruchomienia. Efekt działania tego programu widać na rysunku 2.6. Od razu rzuca się w oczy jeden problem: liczby zmiennoprzecinkowe przedstawiane są zawsze z dokładnością do sześciu miejsc po kropce dziesiętnej. Listing 2.7. Wypisywanie na ekranie wartości zmiennych #include #include // Tylko Visual Studio int main() { unsigned int calkowita = 123; float zmiennoprzecinkowa = 345.67f; char znak = 'K'; float Mnozna = 12.0, Mnoznik = 13.5; printf("1:%d 2:%x 3:%f 4:%c 5:%d 6:%x 7:%X\n", calkowita, calkowita, zmiennoprzecinkowa, znak, znak, znak, znak); printf("%f x %f = %f\n", Mnozna, Mnoznik, Mnozna * Mnoznik); _getch(); // Tylko Visual Studio return 0; }

Na szczęście kody formatujące zaczynające się od znaku % (kontrolujące miejsce pojawiania się wartości zmiennych) można bardzo łatwo rozbudować o informację o rozmiarze pola, w którym ma być wyświetlana liczba, oraz o liczbie cyfr po kropce dziesiętnej:  Rozbudowując kod %f do postaci %.nf (np. %.2f), ograniczy się liczbę cyfr po kropce dziesiętnej do n. Ta sama reguła dotyczy kodów %lf oraz %Lf.  Dodatkowa liczba przed kropką — %m.nf — (np. %10.3f) ogranicza do m rozmiar

pola, w którym zapisana zostanie liczba26. Jeżeli cyfr będzie zbyt dużo,

26

Wliczane są w to wszystkie znaki składające się na tekstową reprezentację liczby: symbol znaku, cyfry przed kropką dziesiętną, sama kropka dziesiętna oraz cyfry za kropką dziesiętną.

Część II  Język C

80

Rysunek 2.6. Efekt działania programu przykładowego z listingu 2.7

ograniczenie nie zadziała, ale jeśli mniej, niż określono, przed liczbą zostaną dodane znaki odstępu (spacje), wyrównujące liczbę do prawego marginesu zarezerwowanego na nią pola na ekranie.  Jeżeli dodatkowo sumaryczny rozmiar pola zostanie podany z zerem na początku (np. %010.3f), w razie pozostania wolnego miejsca w zarezerwowanym polu

zostanie ono uzupełnione nie znakami odstępu, lecz zerami.  Jeśli wyświetlana liczba ma być wyrównywana do lewego marginesu

zarezerwowanego pola, deklarację liczby cyfr należy poprzedzić znakiem myślnika — (np. %-10.3f). Z oczywistych względów nie da się połączyć tej opcji z opcją wypełniania pozostałego zarezerwowanego miejsca zerami — wolne miejsce po prawej stronie liczby (jeśli jeszcze zostanie choć jedno wolne pole znakowe) zostanie wypełnione znakami odstępu (spacjami).  Jeżeli nawet w przypadku liczb nieujemnych ma być wyświetlany symbol znaku liczby, zaraz za znakiem % rozpoczynającym kod należy umieścić znak + (np. %+-10.3f).

Te same zasady dotyczą też kodów formatujących liczby całkowite (%i, %d, %x i wszystkie pochodne), z oczywistym wyjątkiem w postaci braku możliwości określenia liczby cyfr po przecinku dziesiętnym.

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

81

Podprogram printf() pozwala osiągnąć doskonałe rezultaty za pomocą jednego wyrażenia języka C, nie dokonuje jednak żadnej weryfikacji liczby i typów podawanych parametrów. Korzystając z niej, należy zatem pamiętać o przestrzeganiu następujących zasad:  Zawsze należy sprawdzać, czy liczba kodów zaczynających się znakiem %

jest identyczna z liczbą zmiennych podanych jako dalsze parametry funkcji. Na szczęście środowisko Microsoft Visual Studio dokonuje takiej weryfikacji zarówno na etapie tworzenia kodu (podkreślenie wadliwego wyrażenia czerwoną falistą kreską), jak i kompilacji (wygenerowanie ostrzeżenia kompilatora).  Zawsze należy sprawdzać, czy kod formatujący podany w tekście odpowiada

typowi zmiennej, której zawartość chce się wyświetlić na ekranie.  Należy pilnować, by przez przeoczenie nie umieścić w przekazywanym tekście jako pierwszy parametr podprogramu printf() pojedynczego znaku %. Jeżeli na ekranie ma się pojawić znak procentu, musi zostać zapisany kodem %%: float UdzialSpamu = (5.0f / 12.0f) * 100.0f; printf("Udzial spamu w poczcie wyniosl: %.0f%%\n", UdzialSpamu);

Rzutowanie typów danych Nie zawsze typ zmiennej wybrany zgodnie z potrzebami jest odpowiedni w czasie obliczeń. Czasem dla oszczędności pamięci tworzy się zmienne typu unsigned char, przechowujące liczby z zakresu tylko od 0 do 255 i choć są one doskonałe do pamiętania niewielkich wartości, operacje arytmetyczne na nich mogą spowodować, że zostanie przekroczony ich dozwolony zakres. Kompilator języka C zabezpiecza Cię przed tym, przekształcając automatycznie zmienne liczbowe do typu zmiennej, w której zapisujesz wynik, dlatego następujący fragment kodu: unsigned unsigned unsigned unsigned

char PojemnoscMagazynu = 120; char LiczbaMagazynow = 7; char Rezerwa = 9; int Miejsca = Rezerwa + PojemnoscMagazynu * LiczbaMagazynow;

mimo że wynik operacji przekroczy zakres zmiennej typu unsigned char, zostanie wykonany poprawnie — wszystkie obliczenia będą przeprowadzane na liczbach typu unsigned int. Czasem jednak może się przydać możliwość ręcznej zmiany liczby jednego typu w inny — najczęściej o większej pojemności. Kilka przypadków, w których jest to nie tyle przydatne, ile niezbędne, zostanie przedstawionych w dalszej części książki. Tymczasowa zmiana typu zmiennej — a raczej poinformowanie kompilatora, że powinien poprzedzić obliczenia konwersją liczby zapisanej w zmiennej na postać o większej pojemności — wymaga jedynie dopisania przed zmienną nazwy żądanego typu zamkniętej w nawiasach okrągłych: unsigned char Zmienna = 200; ……… (unsigned int) Zmienna ……… // To odwołanie do zmiennej spowoduje wcześniejsze // przekształcenie zapisanej w niej liczby z typu // unsigned char na typ unsigned int.

Część II  Język C

82

Zmienne tymczasowe Nie każda zmienna musi zajmować miejsce w pamięci operacyjnej. Niektóre zmienne mają charakter tymczasowy, przechowując np. pośrednie wyniki obliczeń wykorzystywane później kilkakrotnie w dalszych obliczeniach. Listing 2.8 zawiera prosty program, w którym cena jednostkowa netto elementu jest obliczana jednokrotnie na podstawie liczby sztuk i ceny zamówienia całościowego, a następnie na bazie ceny jednostkowej netto wyznaczana jest cena brutto oraz cena netto zamówienia na dowolną liczbę sztuk elementu. Rysunek 2.7 prezentuje efekt działania tego programu. Listing 2.8. Przykład zastosowania zmiennej w charakterze tymczasowym #include #include // Tylko Visual Studio int main() { float CenaZamowieniaNetto = 1452.0f; unsigned int LiczbaSztuk = 100; float CenaSztuki, CenaBrutto, CenaZamowieniaNetto2; CenaSztuki = CenaZamowieniaNetto / LiczbaSztuk; CenaBrutto = CenaSztuki * 1.23f; CenaZamowieniaNetto2 = CenaSztuki * 200; printf(" Cena netto jednej sztuki: %10.2f zl\n", CenaSztuki); printf("Cena brutto jednej sztuki: %10.2f zl\n", CenaBrutto); printf(" Cena netto na 200 sztuk: %10.2f zl\n", CenaZamowieniaNetto2); _getch(); // Tylko Visual Studio return 0; }

Powyższy program jest poprawny, jednak można go jeszcze udoskonalić. Zmienna CenaSztuki w powyższym przykładzie nie przechowuje wartości trwałej. Jest wykorzystywana wyłącznie po to, by dwukrotnie nie wyznaczać tej samej wartości (ceny netto jednej sztuki). Można zasugerować zatem kompilatorowi, aby nie rezerwował pamięci na zmienną, a w miarę możliwości starał się wykorzystać po prostu wynik obliczeń dwukrotnie. Można to uzyskać, zamieniając wiersz: float CenaSztuki, CenaBrutto, CenaZamowieniaNetto2;

na dwa następujące wiersze, z których pierwszy — definiujący zmienną CenaSztuki — przez zastosowanie słowa kluczowego register przed nazwą typu danych tworzy zmienną tymczasową: register float CenaSztuki; float CenaBrutto, CenaZamowieniaNetto2;

W praktyce w powyższym przykładzie wszystkie zmienne można potraktować jako tymczasowe i opatrzyć słowem kluczowym register. Nie przyniesie to jednak żadnego

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

83

Rysunek 2.7. Efekt działania programu przykładowego z listingu 2.8

dalszego zysku, zaciemniając jedynie program. Należy zaszczepić w sobie zwyczaj opisywania słowem kluczowym register jedynie tych zmiennych, które faktycznie odgrywają tymczasową rolę 27.

Pobieranie informacji o zmiennych Podczas pisania tekstu źródłowego programu zmienne definiuje się podając typ danych oraz nazwę. Po kompilacji to się zmienia diametralnie, a parametrami charakteryzującymi zmienne stają się:  adres, czyli numer komórki pamięci operacyjnej, od której zaczyna się obszar

pamięci zarezerwowany na przechowywanie informacji zapisywanych w zmiennej;  rozmiar, czyli liczba zarezerwowanych bajtów. 27

W praktyce słowo kluczowe register stanowi dla kompilatora sugestię, by dana zmienna nie była w ogóle przechowywana w pamięci, lecz w jednym z rejestrów mikroprocesora. Ponieważ liczba rejestrów jest ograniczona, zbyt rozrzutne stosowanie słowa kluczowego register nie przyniesie żadnego pożytku lub wręcz zaszkodzi.

Część II  Język C

84

Należy podkreślić, że zmienne tymczasowe (zdefiniowane z wykorzystaniem słowa kluczowego register) nie mają adresu, ponieważ kompilator może zdecydować o wykorzystaniu wartości zmiennej bez zapisywania tej wartości w pamięci operacyjnej. Do pobrania adresu zmiennej służy operator &. Adres można wyświetlić bezpośrednio na ekranie, korzystając z kodu formatującego %p podprogramu printf(): float Zmienna; printf("Zmienna ma adres %p\n", &Zmienna);

lub zapisać w tzw. zmiennej wskaźnikowej, którą definiuje się analogicznie jak zwykłą zmienną, lecz nazwę zmiennej poprzedza się znakiem gwiazdki (tylko w wierszu definicji): float Zmienna; float *AdresZmiennej; AdresZmiennej = &Zmienna;

Zmienne wskaźnikowe są intensywnie wykorzystywane przy dynamicznym gospodarowaniu pamięcią operacyjną oraz tworzeniu bardziej rozbudowanych struktur danych, o czym będzie mowa w dalszych rozdziałach książki. Pomiar rozmiaru bloku pamięci zajmowanego przez zmienną można przeprowadzić za pomocą operatora sizeof, zwracającego liczbę bajtów (a dokładniej: liczbę elementów typu char) zajmowanych przez zmienną. Ta wartość nie jest zbytnio interesująca w przypadku pojedynczych zmiennych, gdyż z góry wiadomo, że zmienna typu unsigned char zajmie jeden bajt pamięci, a signed short zazwyczaj dwa bajty28. Operator sizeof staje się naprawdę przydatny dopiero, gdy korzysta się z deklarowanych przez programistę skomplikowanych struktur danych. Argument operatora sizeof podaje się w nawiasie29. Argumentem może być nazwa zmiennej lub dowolnego typu danych: int Rozmiar; double Zmiennoprzecinkowa; Rozmiar = sizeof(float); Rozmiar = sizeof(Rozmiar); Rozmiar = sizeof(Zmiennoprzecinkowa);

Listing 2.9 zawiera tekst źródłowy programu, którego zadaniem jest wyświetlenie na ekranie rozmiarów wszystkich standardowych typów zmiennych języka C. Na rysunku 2.8 znajduje się wydruk efektów jego działania: widać, że zmienne typu char zawsze zajmują blok pamięci o rozmiarze 1 (jest to wymuszone przez standard języka C), a w tej akurat implementacji kompilatora zmienne typu short zajmują 2 bajty (16 bitów), long — 4 bajty (32 bity), a long long — 8 bajtów (64 bity). Można się też przekonać,

28

Choć — jak zostało to już wspomniane — w niektórych implementacjach języka C (a szczególnie na niektórych platformach sprzętowych) rozmiar typów danych pojemniejszych niż char może być inny. 29 Zwyczajowo. Dopuszczalna jest pisownia bez nawiasów, w której argument podaje się po nazwie operatora sizeof i znaku odstępu (spacji).

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

85

że — zgodnie ze standardem — zmienne typu signed zajmują dokładnie tyle samo miejsca w pamięci, co zmienne typu unsigned, a więc wybór między nimi musi być dokonywany na podstawie realnych potrzeb, gdyż nie prowadzi do żadnych oszczędności. Wyświetlony został również rozmiar typu wskaźnikowego void*. Słowo kluczowe void oznacza nic; zmienna wskaźnikowa void* przechowuje adres komórki pamięci nieprzechowującej niczego, co da się sensownie zinterpretować. Typ wskaźnikowy void* jest używany zawsze tam, gdzie nie interesuje nas, co jest pod danym adresem w pamięci, lecz tylko sam adres. Ponieważ wszystkie zmienne wskaźnikowe mają ten sam rozmiar, w tym przypadku void* jest typem reprezentatywnym dla wszystkich wskaźników. Jak widać, na komputerze o 32-bitowej architekturze zmienna wskaźnikowa ma rozmiar 4 B (bajtów), czyli 32 b (bitów). Dodatkowo program ten zawiera przykład zastosowania operatora & w celu pobrania i wyświetlenia adresu zmiennej Zmienna zdefiniowanej na początku bloku. Raz adres wyświetlany jest bezpośrednio, a raz — za pośrednictwem zmiennej wskaźnikowej Adres. Listing 2.9. Przykład zastosowania operatorów & oraz sizeof #include #include // Tylko Visual Studio int main() { int Zmienna = 5; int *Adres = &Zmienna; printf(" signed unsigned\n"); printf(" char %8i %8i\n", sizeof(signed char), sizeof(unsigned char)); printf(" short %8i %8i\n", sizeof(signed short), sizeof(unsigned short)); printf(" int %8i %8i\n", sizeof(signed int), sizeof(unsigned int)); printf(" long %8i %8i\n", sizeof(signed long), sizeof(unsigned long)); printf(" long long %8i %8i\n", sizeof(signed long long), sizeof(unsigned long long)); printf(" float %8i -\n", sizeof(float)); printf(" double %8i -\n", sizeof(double)); printf("long double %8i -\n", sizeof(long double)); printf(" void* - %8i\n", sizeof(void*)); printf("\nAdres zmiennej Zmienna: %p %p\n", &Zmienna, Adres); _getch(); // Tylko Visual Studio return 0; }

Część II  Język C

86

Rysunek 2.8. Efekt działania programu z listingu 2.9

Pobieranie danych z klawiatury Nawet bardzo proste programy mogą realizować pożyteczne obliczenia. Ważne jest jednak, by dane do tych obliczeń nie musiały być wprowadzane wprost do tekstu źródłowego programu, tylko mogły być wpisywane przez użytkownika programu z klawiatury i zapisywane do wyznaczonych w tym celu zmiennych. Podstawowym podprogramem służącym do odczytywania danych jest scanf(). Jest on działającym w drugą stronę odpowiednikiem podprogramu printf(): pobiera dane z klawiatury i umieszcza w obszarach pamięci przydzielonych zmiennym. Korzysta przy tym nawet z tych samych kodów określających typ odczytywanej danej. Na przykład by odczytać z klawiatury liczbę i umieścić ją w zmiennej typu unsigned long int, należy użyć następujących dwóch wyrażeń (pierwsze definiuje zmienną, drugie wpisuje do niej wartość odczytaną z klawiatury): unsigned long Zmienna; scanf("%lu", &Zmienna);

Podstawowa różnica między wywołaniem podprogramów printf() i scanf() polega na tym, że w przypadku scanf() wszystkie argumenty będące nazwami zmiennych muszą zostać poprzedzone symbolem operatora &, nakazującego kompilatorowi przekazanie

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

87

nie wartości zmiennej, ale adresu obszaru pamięci operacyjnej przydzielonego zmiennej. Pod przekazanym adresem podprogram scanf() dokona zapisu danych wprowadzonych z klawiatury. Pomyłka polegająca na pominięciu operatora & jest łatwa do popełnienia, częsta i tragiczna w skutkach — podprogram zinterpretuje liczbę zapisaną w zmiennej jako adres komórki pamięci i w dobrej wierze dokona pod ten adres zapisu danych, powodując natychmiastowy błąd programu (rysunek 2.9). Rysunek 2.9. Efekt błędu polegającego na pominięciu symbolu operatora & w wywołaniu podprogramu scanf()

Z oczywistych względów nie można też używać jako parametrów podprogramu scanf() wyrażeń algebraicznych: dane muszą zostać zapisane do zmiennej, a wyrażenie algebraiczne nie jest zmienną i ma tylko wyliczoną wartość, nie dysponując blokiem pamięci, do którego mogłyby zostać zapisane odczytane z klawiatury dane. Mówiąc krótko: każdy z parametrów scanf() (poza pierwszym, określającym format odczytywanych danych) musi być nazwą zmiennej poprzedzoną znakiem &. Na listingu 2.10 przedstawiony został tekst źródłowy programu obliczającego obwód i pole powierzchni koła o zadanym promieniu, zaś na rysunku 2.10 — wynik działania programu. Aby program znów był nieco bardziej skomplikowany, tym razem za pomocą dyrektywy #include dołączana jest do programu deklaracja wartości stałych oraz podprogramów wykorzystywanych w obliczeniach matematycznych. Dzięki temu nie trzeba wprowadzać ręcznie wartości liczby π, lecz można skorzystać z wartości stałej M_PI o wartości równej w przybliżeniu π z najwyższą możliwą w danym przypadku dokładnością. Niestety, aby można było skorzystać z tej wartości30, przed wierszem #include musi się znaleźć dodatkowy wiersz o treści: 30

To ograniczenie dotyczy tylko kompilatora zawartego w środowisku Microsoft Visual Studio. Inne kompilatory udostępniają zazwyczaj symbole matematyczne zawarte w pliku nagłówkowym math.h bez konieczności wprowadzania dodatkowych wyrażeń preprocesora.

Część II  Język C

88 #define _USE_MATH_DEFINES

informujący, że mają zostać użyte definicje wartości stałych zawarte w zbiorze nagłówkowym math.h. Listing 2.10. Tekst źródłowy programu obliczającego obwód i pole powierzchni koła o długości promienia wprowadzanej z klawiatury #define _USE_MATH_DEFINES // Tylko Visual Studio #include #include #include int main() { // Definicje zmiennych. float Promien; // Dane wejściowe. printf("Wprowadz promien kola: "); scanf("%f", &Promien); // Wyprowadzenie wyników obliczeń. printf("\nObwod kola o promieniu %.2f wynosi %.2f\n", Promien, 2.0 * M_PI * Promien); printf("Powierzchnia kola o promieniu %.2f wynosi %.2f\n", Promien, M_PI * Promien * Promien); // Podziękowanie i zakończenie programu. printf("\nDziekuje za wspolprace!\n"); _getch(); // Tylko Visual Studio return 0; }

Po przyglądnięciu się tekstowi źródłowemu powyższego programu można odszukać w nim cztery podstawowe części:  część definicyjną, w której znajdują się wyrażenia definiujące wszystkie

zmienne używane przez program;  część odczytującą dane, w której do niektórych wcześniej zadeklarowanych

zmiennych wprowadza się (za pomocą podstawiania lub odczytywania danych z klawiatury) dane potrzebne do dalszej pracy programu;  część przetwarzającą dane, w której na podstawie zmiennych wypełnionych

odczytanymi danymi program przeprowadza obliczenia, a ich wyniki wpisuje do przygotowanych wcześniej zmiennych;  część wyprowadzającą wyniki, wyświetlającą wyniki na ekranie komputera.

Własne programy obliczeniowe warto tworzyć według powyższego szablonu, dzieląc je na cztery wykonywane po kolei części. Jedynie w niektórych przypadkach, gdy część przetwarzająca dane jest bardzo prosta, można ją połączyć z częścią wyprowadzającą wyniki. Poszczególne bloki programu oraz większe grupy instrukcji warto przy tym rozdzielać pustymi wierszami oraz uzupełniać komentarzami. Podprogram scanf() nie sprawdza się jednak w sytuacji, gdy konieczne jest odczytanie z klawiatury jednego znaku, odpowiadającego naciśniętemu klawiszowi. Uzupełnia

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

89

Rysunek 2.10. Efekt działania programu z listingu 2.10

go w tym zakresie podprogram _getch(), czekający na naciśnięcie klawisza i zwracający kod wprowadzonego znaku. Choć ze względu na wstrzymywanie pracy programu do czasu naciśnięcia klawisza przez użytkownika ten podprogram nie nadaje się do tworzenia gier lub implementacji mechanizmu przerywania trwających obliczeń, doskonale sprawdza się przy tworzeniu menu dających użytkownikowi możliwość wyboru jednej z kilku opcji. Podprogram _getch() był już wykorzystywany w dotychczas przedstawionych programach przykładowych w celu opóźnienia zamknięcia programu do momentu naciśnięcia dowolnego klawisza przez użytkownika. Dotychczas jednak wynik zwracany przez podprogram — czyli kod naciśniętego klawisza — był przez programy ignorowany. Na listingu 2.11 przedstawiony został tekst źródłowy programu oczekującego na naciśnięcie dowolnego klawisza, zapamiętującego kod wprowadzonego w ten sposób znaku w zmiennej Litera i wyświetlającego ten znak z powrotem na ekranie. Rysunek 2.11 prezentuje efekt uruchomienia programu. Listing 2.11. Tekst źródłowy programu wczytującego kod znaku naciśniętego przez użytkownika na klawiaturze #include #include // Tylko Visual Studio int main()

Część II  Język C

90 { char Znak; printf("Nacisnij dowolny klawisz: "); Znak = _getch(); printf("\nNacisnales klawisz [%c]!\n", Znak); _getch(); // Tylko Visual Studio return 0; }

Rysunek 2.11. Efekt działania programu z listingu 2.11

Śledzenie realizacji programu Programista podczas tworzenia lub rozbudowywania programu musi stawić czoło dwóm rodzajom błędów. Błędy składniowe, wspomniane już w poprzednim rozdziale, są wykrywane przez kompilator i choć uniemożliwiają one uruchomienie programu, względnie łatwo jest je odszukać (pomaga w tym panel listy błędów) i poprawić. Bardziej uciążliwą kategorią błędów są błędy logiczne. Program zawierający błąd logiczny będzie działał i może nawet dawać jakieś wyniki. Ba, czasem wyniki mogą być

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

91

nawet prawdopodobne lub wręcz poprawne dla pewnych danych testowych. Po podaniu innych danych program jednak podaje błędne wyniki lub wręcz jest przerywany z komunikatem błędu31. Przy usuwaniu tego typu błędów nieoceniona jest możliwość realizacji programu w sposób krokowy. Nakazując wykonywanie programu wiersz po wierszu, programista jest w stanie na bieżąco śledzić wartości zmiennych i ocenić, w którym miejscu może pojawić się błąd. Można zaryzykować stwierdzenie, że przy tworzeniu większych programów umiejętność skutecznego korzystania z narzędzi uruchomieniowych środowiska programistycznego jest prawie tak ważna, jak umiejętność programowania.

Krokowa realizacja programu Po wybraniu z menu Debuguj opcji Start Debugging32 rozpoczyna się realizacja programu. Programista nie wie jednak, który wiersz programu jest w danej chwili wykonywany ani jaka jest zawartość zmiennych. Informacja taka nie miałaby zresztą większej wartości, jeśli brać pod uwagę tempo wykonywania instrukcji przez mikroprocesor: w każdej sekundzie wykonanych może być kilkadziesiąt lub kilkaset tysięcy wierszy tekstu źródłowego programu. Wystarczy jednak użyć opcji Step Over menu Debuguj33, by rozpocząć krokową realizację programu. W trybie pracy krokowej za każdym razem wykonywany jest tylko jeden wiersz tekstu źródłowego programu. Po jego realizacji program jest przerywany, a programista ma możliwość wglądu w zawartość zmiennych przed przejściem do realizowania kolejnego wiersza. Skorzystanie ze wspomnianej opcji menu uruchamia program, jednak sterowanie wraca prawie natychmiast do okna środowiska programistycznego. W panelu edycji tekstu źródłowego wyświetlana jest zawartość zbioru źródłowego zawierającego podprogram główny main(), a przy pierwszym wierszu tego podprogramu pojawia się żółta strzałka, symbolizująca wiersz, który po kolejnym skorzystaniu z opcji Step Over (lub naciśnięciu klawisza F10) zostanie zrealizowany (rysunek 2.12). Od tego momentu do dyspozycji programisty są następujące opcje menu Debuguj i odpowiadające im klawisze funkcyjne:  Step Over (F10) — realizuje kod odpowiadający wierszowi tekstu źródłowego

wyróżnionemu żółtą strzałką na marginesie;  Step Into (F11) — realizuje kod odpowiadający wierszowi tekstu źródłowego

wyróżnionemu żółtą strzałką na marginesie, jednak jeżeli wiersz ten odwołuje 31

Najprostszym przykładem błędu logicznego jest program obliczający wynik z uwzględnieniem operacji dzielenia przez liczbę podawaną przez użytkownika z klawiatury. Program będzie działał poprawnie do momentu wprowadzenia przez użytkownika liczby zero; podczas próby dzielenia przez zero system operacyjny przerwie jego działanie i wyświetli pole dialogowe z komunikatem błędu. 32 Lub po naciśnięciu klawisza F5. 33 Lub nacisnąć klawisz F10.

Część II  Język C

92

Rysunek 2.12. Żółta strzałka oznacza wiersz przeznaczony do realizacji w kolejnym kroku

się do podprogramów znajdujących się w analizowanym programie, następuje przejście do pierwszego wiersza tekstu źródłowego wywoływanego podprogramu i oczekiwanie na kolejną decyzję programisty;  Step Out (Shift+F11) — automatycznie realizuje wszystkie pozostałe instrukcje

danego podprogramu, ale ponownie przerywa działanie programu (pozwalając programiście dalej analizować jego działanie krokowo) w momencie powrotu do podprogramu, który spowodował wywołanie analizowanego w danej chwili podprogramu;  Continue (F5) — kończy tryb pracy krokowej i rozpoczyna kontynuację działania

programu w trybie pracy ciągłej.

Podgląd zawartości zmiennych Aby podglądnąć zawartość zmiennej w czasie, gdy działanie programu jest wstrzymane (środowisko programistyczne znajduje się w trybie pracy krokowej), wystarczy umieścić wskaźnik myszy nad nazwą zmiennej w tekście źródłowym programu. Po chwili pojawi się w tym miejscu pole prezentujące zawartość zmiennej (rysunek 2.13). W zależności od typu zmiennej format wyświetlanych danych może być różny, jednak środowisko programistyczne zostało zaprojektowane w ten sposób, by forma prezentowanych w ten sposób danych była jak najbardziej przydatna dla programistów analizujących działanie programów.

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

93

Rysunek 2.13. Podgląd zawartości zmiennej bezpośrednio w polu edycyjnym środowiska programistycznego

Jeśli więcej zmiennych wymaga śledzenia lub programista pragnie śledzić zmiany zmiennej podczas realizacji kolejnych wierszy tekstu źródłowego bez ciągłego sięgania po mysz34, warto skorzystać z panelu śledzenia, automatycznie otwieranego po uruchomieniu programu (rysunek 2.14). Trzy najczęściej wykorzystywane zakładki tego panelu to:  Autos — prezentuje listę zmiennych, które według środowiska programistycznego

są wykorzystywane w danym fragmencie programu i programista może mieć potrzebę śledzenia ich zawartości35;  Locals — prezentuje wszystkie zmienne lokalne, czyli zdefiniowane w danym

podprogramie lub bloku instrukcji;  Watch 1 — umożliwia dowolną edycję listy zmiennych, których zawartość

jest podglądana. Rysunek 2.14. W lewym dolnym rogu — panel śledzenia zawartości zmiennych z aktywną zakładką Autos

34

Początkujący programista powinien jak najszybciej nauczyć się obsługiwać środowisko programistyczne z jak najmniejszym udziałem myszy. Wykorzystywane najczęściej funkcje wywołuje się najszybciej za pomocą kombinacji klawiszy. 35 Aby wyświetlić tę zakładkę, z menu Debuguj należy wybrać kolejno pozycję Okna i Autos.

Część II  Język C

94

Pułapki W przypadku większych programów korzystanie z trybu pracy krokowej, by dotrzeć do fragmentu wymagającego analizy, byłoby jeśli nie niemożliwe, to niezmiernie uciążliwe. Istnieje jednak możliwość ustawienia w wybranym wierszu tekstu źródłowego pułapki (ang. breakpoint36). Ustawienie pułapki w jednym z wierszy tekstu źródłowego programu oznacza, że osiągnięcie przez program tego wiersza w czasie jego realizacji spowoduje przerwanie pracy i wejście w tryb pracy krokowej. Aby zastawić pułapkę w wybranym wierszu tekstu źródłowego programu, należy ustawić kursor w tym wierszu i z menu Debuguj wybrać pozycję Toggle Breakpoint37. Pułapkę symbolizuje czerwone kółko wyświetlane na lewym marginesie tekstu źródłowego programu (rysunek 2.15). Ponowne wywołanie tej opcji menu (lub użycie klawisza F9) usuwa pułapkę zastawioną w bieżącym wierszu. Rysunek 2.15. Zastawioną pułapkę symbolizuje czerwone kółko na lewym marginesie tekstu źródłowego Mechanizm pułapek jest o wiele potężniejszy, niż wynika z powyższego, z konieczności skrótowego opisu. Pułapki mogą być zależne od ustalonego przez programistę warunku, zatrzymując program jedynie w określonych okolicznościach. Programista może też określać działania podejmowane, gdy program wpadnie w zastawioną pułapkę — inne niż tylko wejście w tryb pracy krokowej. Można też zliczać, ile razy program wpadł w daną pułapkę, co może się przydać przy analizowaniu wydajności programu. Ponieważ dokładny opis wszystkich funkcji udostępnianych przez środowisko Microsoft Visual Studio nie wchodzi w zakres tej książki, Czytelnicy zainteresowani wspomnianymi funkcjami powinni sięgnąć po literaturę poświęconą wyłącznie temu środowisku.

Podsumowanie Przedstawione w tym rozdziale podstawy programowania w języku C pozwalają tworzyć jedynie najprostsze, choć już użyteczne narzędzia. Najważniejsze, by po lekturze rozdziału Czytelnik był w stanie napisać dowolny własny program według schematu: „odczytaj dane, przetwórz je w prosty sposób i wypisz na ekranie”.

36

W dosłownym tłumaczeniu: punkt przerwania. Polskie słowo „pułapka” lepiej jednak oddaje istotę tej funkcji środowiska programistycznego. 37 Tej operacji odpowiada klawisz funkcyjny F9.

Rozdział 2.  Wprowadzanie, wyprowadzanie i przechowywanie danych

Oto podsumowanie najważniejszych zagadnień przedstawionych w tym rozdziale:  Każde wyrażenie języka C kończy się znakiem średnika.  Treść programu wpisuje się wewnątrz podprogramu głównego main(), pomiędzy nawiasami klamrowymi { i }.  Dodatkowy opis działania programu, ułatwiający późniejsze zrozumienie jego

treści autorowi oraz innym programistom umieszcza się jako komentarz.  Podprogram printf() pozwala nakazać komputerowi wyświetlenie dowolnego

tekstu oraz wartości wskazanych zmiennych wedle podanego jej jako pierwszy parametr szablonu tekstowego.  Zmienne deklaruje się na początku bloku instrukcji (za nawiasem klamrowym { otwierającym blok).  Odczytywanie danych z klawiatury i umieszczanie ich we wskazanych zmiennych realizuje podprogram scanf(), z którego korzysta się podobnie jak z printf().  Wszystkie nazwy zmiennych podawane jako parametry funkcji scanf() muszą być poprzedzone znakiem &, pobierającym adres zmiennej (numer początkowej

komórki pamięci).  Podanie przed nazwą zmiennej nazwy typu zamkniętej w nawiasy okrągłe ((typ) zmienna) nakazuje kompilatorowi przekształcenie wartości zmiennej

do podanego typu (najczęściej o większym zakresie liczb) przed dokonaniem obliczeń.  Za pomocą operatora sizeof można odczytać rozmiar bloku pamięci

zajmowanego przez dowolną istniejącą zmienną lub hipotetyczną zmienną określonego typu.  Wykorzystanie funkcji śledzenia realizacji programu znacząco ułatwia

zrozumienie sposobu jego działania oraz odszukanie błędów w tekście źródłowym powodujących uzyskiwanie błędnych wyników obliczeń lub pojawianie się błędów realizacji programu zgłaszanych przez system operacyjny.

95

96

Część II  Język C

Rozdział 3.

Konstrukcje warunkowe i typy wyliczeniowe W poprzednim rozdziale prezentowane były programy o przebiegu liniowym. Realizacja ich kodu rozpoczyna się w ściśle określonym miejscu i przebiega całkowicie sekwencyjnie (jedna instrukcja po drugiej). W momencie dotarcia do końca bloku instrukcji podprogramu głównego main() (lub wykonania instrukcji return) działanie programu kończy się. W rzeczywistości programy takie należą do rzadkości. Program musi prawie zawsze reagować na pewne specjalne sytuacje, wykonując jeden z kilku bloków instrukcji w zależności od tego, jaki zestaw warunków spełniają dane wejściowe lub pośrednie wyniki obliczeń. Podczas wprowadzania danych przez użytkownika program powinien weryfikować podawane informacje i zgłaszać wszelkie znalezione nieprawidłowości. Niektóre zastosowania komputerów wymagają też przeprowadzania powtarzalnych obliczeń na dużych blokach danych tego samego typu. Próba zrealizowania wszystkich tych wymagań za pomocą programu o przebiegu liniowym jest z góry skazana na niepowodzenie. Język C, jak każdy rozbudowany język wysokiego poziomu, daje jednak do dyspozycji zestaw narzędzi umożliwiających wprowadzenie do programu fragmentów, które są wykonywane w sposób powtarzalny (wielokrotny, w pętli) lub warunkowy (zależny od zestawu warunków) oraz upraszczają obróbkę dużych ilości danych tego samego typu. W poniższym rozdziale omówione zostaną następujące z nich:  zmienne i wyrażenia logiczne — umożliwiające opisanie warunków,

wyznaczenie wartości logicznej i przechowanie jej do późniejszego wykorzystania;  instrukcje warunkowe — umożliwiające wykonywanie jednego z dwóch

(lub więcej) bloków instrukcji w zależności od wartości wskazanych zmiennych i warunków, które te wartości spełniają;  operator warunkowy — umożliwiający wybór jednej z dwóch wyliczonych

lub zapamiętanych wartości w zależności od warunku;

Część II  Język C

98

 typy wyliczeniowe — pozwalające na opisanie wielu możliwych opcji

należących do skończonego zbioru i zapisywanie wartości związanych z tymi opcjami;  konstrukcje warunkowe — stanowiące czytelniejszą alternatywę dla obszernych sekwencji instrukcji warunkowych if i stosowane w przypadku, gdy należy

zareagować na wybór jednej wartości ze skończonego zbioru.

Zmienne i wyrażenia logiczne Warunki, pod którymi część programu ma być realizowana, mogą często przybierać dość skomplikowaną formę. Na przykład w programie obsługującym sklep opcja usuwania towaru ze stanu magazynowego może być aktywna jedynie, gdy podświetlona jest co najmniej jedna pozycja listy towarów, a zarazem wszystkie podświetlone pozycje listy odpowiadają towarom o zerowym stanie magazynowym. Aby warunki tego typu można było zapisać w programie, należy użyć wyrażeń logicznych. Raz wyznaczone wartości logiczne czasem warto przechować i wykorzystać następnym razem, jeżeli nie zmieniły się. Do zapisywania wartości logicznych — podobnie jak w przypadku liczb — służą zmienne logiczne. I choć wartości logiczne są tak naprawdę liczbami, w najnowszym standardzie języka C przewidziano dla nich oddzielny typ danych.

Wartości logiczne Tak jak w matematyce, w informatyce przyjmuje się istnienie wyłącznie dwóch wartości logicznych:  fałsz o wartości 0 odpowiada stanowi nieaktywnemu, wyłączonemu;  prawda o wartości 1 odpowiada stanowi aktywnemu, włączonemu.

Warto zauważyć, że te wartości odpowiadają dokładnie dwóm możliwym stanom bitów w systemie binarnym. Jeden bit informacji można traktować właśnie jako najmniejszą możliwą wartość logiczną: bit może być albo „zapalony” (stan 1), albo „zgaszony” (stan 0), co odpowiada jednej z dwóch powyżej przedstawionych wartości logicznych.

Zmienne logiczne W najprostszym przypadku zmienną logiczną może być po prostu zmienna typu całkowitego. W przeszłości często używało się w tym celu typu int, równie dobrze można jednak zastosować typy short int oraz char (przy czym mogą zajmować mniej pamięci operacyjnej niż int). Na przykład jeżeli zdefiniuje się zmienną: int Wynik = 0;

Rozdział 3.  Konstrukcje warunkowe i typy wyliczeniowe

99

to odwołanie się do nazwy zmiennej Wynik będzie wyrażeniem logicznym, którego wartość będzie równa aktualnej wartości tej zmiennej (w powyższym przykładzie fałszowi — 0). Zmienną, której wartość interpretuje się jako wartość logiczną (prawda lub fałsz), nazywa się zmienną logiczną lub zmienną dwustanową (dwuwartościową). W najnowszym standardzie języka C1 wprowadzono specjalizowany typ logiczny bool, który jest przeznaczony wyłącznie do zapisu wartości logicznych. Zmienne typu bool mogą przyjmować tylko jedną z dwóch wartości:  false — wartość 0 odpowiada fałszowi (warunek niespełniony);  true — wartość 1 odpowiada prawdzie (warunek spełniony).

Aby użyć typu logicznego bool, do programu należy dołączyć plik nagłówkowy stdbool.h. Od tego momentu można korzystać zarówno z samego typu, jak i z literałów true i false: #include bool Wynik = false;

Niestety, kompilator zawarty w pakiecie Microsoft Visual Studio 2012 nie obsługuje najnowszej wersji standardu języka C. W tym jednym przypadku można obejść problem, dołączając do swojego programu następujący wycinek kodu: typedef int bool; #define false 0 #define true 1

lub umieszczając go w oddzielnym pliku nagłówkowym (choćby o nazwie stdbool.h) i dołączając do tworzonych projektów. Pełną treść takiego pliku nagłówkowego przedstawiono na listingu 3.1. Listing 3.1. Zawartość pliku nagłówkowego stdbool.h #ifndef #define #define #ifndef typedef #define #define #endif #endif

_STDBOOL_H_ _STDBOOL_H_ __bool_true_false_are_defined 1 __cplusplus int bool; false 0 true 1

Wyrażenia logiczne Wyrażenia logiczne to wyrażenia algebraiczne, których wynikiem jest wartość logiczna true (1) lub false (0). Ponieważ operacje logiczne różnią się — mimo podobnych często nazw — od operacji arytmetycznych, w języku C, podobnie jak w języku

1

Standard C99, zdefiniowany w dokumencie ISO/IEC 9899:1999.

Część II  Język C

100

matematyki, określony jest oddzielny zbiór operatorów logicznych. Ich lista, wraz z wytłumaczeniem znaczenia, znajduje się w tabeli 3.1. Tabela 3.1. Operatory logiczne w języku C Symbol

Nazwa

Przykład

Znaczenie

==

równość

a == b

Zwraca true, jeżeli dwie porównywane wartości są równe; w przeciwnym przypadku false. Obydwie wartości muszą być tego samego typu lub typów zgodnych.

!=

różność

a != b

Zwraca true, jeżeli dwie porównywane wartości są różne; w przeciwnym przypadku false. Obydwie wartości muszą być tego samego typu lub typów zgodnych.


=

większe lub równe

a >= b

Zwraca true, jeżeli pierwsza z wartości jest większa lub równa drugiej; w przeciwnym przypadku false. Obydwie wartości muszą być tego samego typu lub typów zgodnych.

!

negacja

!a

Zwraca wartość logiczną przeciwną do podanej: true, jeżeli a jest równe false lub false, jeżeli a jest równe true. Wartość a musi być wartością logiczną.

||

suma

a || b

Zwraca true, jeżeli choć jeden z argumentów ma wartość true; w przeciwnym przypadku false. Obydwie wartości muszą być wartościami logicznymi.

&&

iloczyn

a && b

Zwraca true, jeżeli obydwa argumenty mają jednocześnie wartość true; w przeciwnym przypadku false. Obydwie wartości muszą być wartościami logicznymi.

^

suma modulo

a ^ b

Zwraca true, jeżeli tylko jeden z dwóch argumentów ma wartość true; w przeciwnym przypadku (obydwa true lub obydwa false) zwraca false. Obydwie wartości muszą być wartościami logicznymi.

Operatory logiczne można łączyć w większe ciągi, a także grupować, wykorzystując nawiasy okrągłe. O kolejności wykonywania operacji w wyrażeniu logicznym decyduje priorytet operatora zgodny z zasadami obowiązującymi w matematyce. Kolejno realizowane są operacje: 1) umieszczone w nawiasie, 2) negacji, 3) porównania wartości logicznych, 4) sumy modulo,

Rozdział 3.  Konstrukcje warunkowe i typy wyliczeniowe

101

5) iloczynu logicznego, 6) sumy logicznej.

Do dobrego zwyczaju należy — nawet jeżeli w danym przypadku nie jest to potrzebne — grupowanie poszczególnych składników rozbudowanych wyrażeń logicznych za pomocą nawiasów. Choć dwa poniższe wyrażenia Wynik1 oraz Wynik2 są równoznaczne i poprawne: int Liczba1, Liczba2; bool Wynik1, Wynik2; ... Wynik1 = Liczba1 == Liczba2 || Liczba1 > 0 && Liczba2 > 0; Wynik2 = ((Liczba1 == Liczba2) || ((Liczba1 > 0) && (Liczba2 > 0)));

drugie z nich zdecydowanie łatwiej jest przeczytać, rozszyfrować i zrozumieć. Należy zwrócić szczególną uwagę — przede wszystkim na początku uczenia się języka C — na różnicę między operatorem przypisania = i porównania ==. Zastąpienie operatora porównania operatorem przypisania nie jest błędem, jeśli chodzi o składnię języka C, lecz zmienia całkowicie sens programu. Wyrażenie (Liczba == 0) zwraca wartość true, jeżeli zmienna Liczba zawiera liczbę 0 (i false w przeciwnym przypadku); tymczasem wyrażenie (Liczba = 0) zmienia wartość zmiennej Liczba na 0 (ponieważ jest to operacja przypisania) i zawsze zwraca wartość logiczną false, gdyż liczba 0 odpowiada fałszowi2. Jak widać na powyższym przykładzie, brak jednego znaku spowodował z jednej strony zafałszowanie danych, a z drugiej uzyskanie błędnego wyniku logicznego, niezależnego od danych wejściowych.

Operatory >= i = Dama) printf("Masz mocna karte!\n");

6

7

Jeżeli takie przypisanie tworzące równoważne opcje wyliczenia jest faktycznie niezbędne, możliwość jego zrealizowania może być bardzo przydatna. Jeżeli jednak zrobi się to przez przypadek, programistę czekają długie wieczory spędzone na sprawdzaniu, dlaczego program nie odróżnia np. kierów od pików. Z tego powodu należy raczej unikać ręcznego nadawania wartości liczbowych wartościom wyliczenia i pozwolić kompilatorowi martwić się o przydzielanie unikatowych wartości, nie wprowadzając samemu potencjalnych błędów w programie. Porównywanie kolejności w ramach definicji typu wyliczeniowego (wykonywane za pomocą operatorów , >= i 6) { printf("Oszukujesz! Masz co najwyzej szesc jablek!\n"); LiczbaJablek = 6; } // Wyświetlenie tekstu o jedzeniu jabłek. switch (LiczbaJablek) { case 6: printf("Zjadam Twoje szoste jablko...\n"); case 5: printf("Zjadam Twoje piate jablko...\n"); case 4: printf("Zjadam Twoje czwarte jablko...\n"); case 3: printf("Zjadam Twoje trzecie jablko...\n"); case 2: printf("Zjadam Twoje drugie jablko...\n"); case 1: printf("Zjadam Twoje pierwsze jablko...\n"); } printf("No i skonczylem zjadac Twoje jablka!\n"); // Koniec programu. _getch(); // Tylko Visual Studio return 0; }

Sekcja default Sekcja default, występująca opcjonalnie wewnątrz konstrukcji switch na samym końcu, za wszystkimi sekcjami case, pełni funkcję podobną do części else wyrażenia warunkowego if. Pozwala ona podać wykonywane instrukcje, jeżeli wartość wyrażenia sterującego konstrukcji switch nie odpowiada żadnej z etykiet case. Listing 3.8 prezentuje prosty program ilustrujący zastosowanie etykiety default. Listing 3.8. Przykład zastosowania etykiety default #include #include // Tylko Visual Studio enum KolorKarty { Trefl, Pik, Kier, Karo

Rozdział 3.  Konstrukcje warunkowe i typy wyliczeniowe

Rysunek 3.7. Efekt działania programu przykładowego z listingu 3.7 }; int main() { enum KolorKarty Kolor = Karo; switch (Kolor) { case Trefl: printf("Masz trefla.\n"); break; case Pik: printf("Masz pika.\n"); break; default: printf("Masz inna karte.\n"); break; } _getch(); // Tylko Visual Studio return 0; }

119

Część II  Język C

120

Podsumowanie Przedstawione w tym rozdziale techniki pozwalają na zróżnicowanie sposobu działania programu w zależności od wprowadzonych przez użytkownika danych lub wyznaczonych w czasie pracy programu wyników pośrednich. Instrukcje warunkowe if są wykorzystywane szczególnie często do weryfikowania poprawności wprowadzanych danych, wyświetlania komunikatów o błędach w danych lub obliczeniach, a także korygowania niektórych danych lub wyników pośrednich. Z kolei konstrukcja warunkowa switch pozwala w sposób elegancki zapisać kod obsługujący wiele różnych przypadków, w tym wymagających (grupowo) obsłużenia w ten sam sposób lub obsługiwanych kaskadowo. Instrukcja switch nabiera szczególnego znaczenia w połączeniu z typami wyliczeniowymi, które pozwalają wprowadzić do kodu pewną abstrakcję opcji do wyboru, odrywając programistę od myślenia wciąż w kategorii liczb i pozwalając mu zapisywać pewne zbiory wartości w postaci opisowych nazw. Oto podsumowanie najważniejszych zagadnień przedstawionych w tym rozdziale:  Wartości logiczne pozwalają przechowywać informację o stanie

dwuwartościowym (tak/nie, prawda/fałsz, włączony/wyłączony).  Wyrażenia logiczne umożliwiają zbudowanie bardziej rozbudowanych zdań

logicznych z wykorzystaniem operatorów porównujących wartości i łączących poszczególne fragmenty zdań logicznych w całość w dowolny sposób.  Instrukcja warunkowa if pozwala wykonać występującą za nią instrukcję

(lub blok instrukcji) pod warunkiem podanym w nawiasach lub pominąć tę instrukcję (lub blok), jeżeli warunek nie jest spełniony.  W połączeniu z instrukcją else instrukcja warunkowa if pozwala na alternatywny

wybór jednej z dwóch instrukcji (lub bloków instrukcji) wykonywanych w zależności od tego, czy warunek podany w nawiasach jest spełniony, czy nie.  Typy wyliczeniowe pozwalają na zastąpienie arbitralnie wybranych wartości

liczbowych opisujących pewne opcje lub stany nazwami, wprost określającymi daną opcję lub stan.  Zmienne wyliczeniowe przechowują wartości wyliczeniowe.  Możliwe jest nadawanie wartościom wyliczeniowym dowolnych wartości

liczbowych oraz dowolne rzutowanie (tłumaczenie) wartości wyliczeniowych na liczbowe i odwrotnie.  Konstrukcja warunkowa switch zastępuje prosty warunek instrukcji if wyborem

jednej z wielu wartości liczbowych lub wyliczeniowych, pozwalając na przypisanie do każdej możliwości (lub grupy możliwości) fragmentu kodu obsługującego dany przypadek.

Rozdział 3.  Konstrukcje warunkowe i typy wyliczeniowe

121

 Każdy fragment kodu obsługujący przypadek (case) instrukcji switch powinien kończyć się instrukcją break przerywającą dalszą obsługę przypadków; instrukcję break wolno pominąć jedynie tam, gdzie celowo program ma kaskadowo

wykonywać kod przypisany do dalszych, niewystępujących w danym momencie przypadków.  Etykieta default jest w instrukcji switch odpowiednikiem instrukcji else

i pozwala na obsłużenie wszystkich przypadków, które nie zostały wprost określone odpowiednimi etykietami case.

122

Część II  Język C

Rozdział 4.

Struktury i unie W dotychczas przedstawianych programach wykorzystywane były wyłącznie wartości tzw. typów prostych. Typ prosty to taki, którego wewnętrzną reprezentacją maszynową jest liczba będąca dla mikroprocesora naturalną postacią informacji. Typami prostymi są typy całkowite, zmiennoprzecinkowe, logiczne, wyliczeniowe i znakowe — a więc wszystkie, które się dotychczas pojawiły1. W realnych zastosowaniach zachodzi często konieczność połączenia kilku wartości opisywanych typami prostymi w jedną całość. Operowanie na zbiorach powiązanych ze sobą — zamiast całkowicie oddzielnych — liczb jest naturalnym sposobem zwiększenia stopnia abstrakcji, a to niesie ze sobą wygodniejsze, szybsze programowanie prowadzące do mniejszej liczby błędów w programie. Zdarza się również czasem, że z kilku wartości (lub zbiorów wartości) wykorzystywana jest tylko jedna wartość (lub jeden zbiór). Rezerwowanie pamięci komputera na wszystkie możliwe wartości stanowi w takim przypadku nieuzasadnioną rozrzutność. Można tego uniknąć przez nałożenie na siebie wszystkich możliwych wartości we wspólnym obszarze pamięci i używanie tylko jednego, wybranego sposobu interpretacji tych komórek pamięci — zgodnie z jednym z równoległych opisów. W tym rozdziale opisane zostaną następujące złożone typy danych występujące w języku C:  struktury pozwalające na powiązanie ze sobą wielu wartości typów prostych

i złożonych, przez występują one w pamięci zawsze w tym samym zestawie i są przetwarzane i przenoszone razem;  unie łączące w jednym obszarze pamięci wiele sposobów interpretacji informacji.

1

Do typów prostych zalicza się również typy wskaźnikowe, opisane pokrótce w rozdziale 2. i szerzej w rozdziale 8. Niektórzy zaliczają też do tej rodziny typów typ void, niereprezentujący sobą żadnej wartości.

Część II  Język C

124

Struktury Jak opisać w programie datę? Wydawałoby się, że to nic trudnego — wystarczy stworzyć trzy zmienne całkowite przechowujące dzień, miesiąc i rok: int Dzien, Miesiac, Rok;

lub — by zaoszczędzić pamięć i zamiast 12 B2 wymaganych do zapisania powyższych trzech liczb użyć tylko 6 B3: unsigned char Dzien, Miesiac; signed int Rok;

ponieważ do zapisania dnia miesiąca (od 1 do 31) i numeru miesiąca (od 1 do 12) całkowicie wystarcza oferowany przez typ unsigned char zakres od 0 do 255 bez liczb ujemnych4. Puryści języka C zastąpią jeszcze w tym przypadku opis miesiąca liczbą typem wyliczeniowym: enum Miesiace { Styczen = 1, Luty, Marzec, Kwiecien, Maj, Czerwiec, Lipiec, Sierpien, Wrzesien, Pazdziernik, Listopad, Grudzien }; unsigned char Dzien; enum Miesiace Miesiac; signed int Rok;

Chociaż taki zapis jest mniej oszczędny5, stosując go, zyskuje się na czytelności i zachowuje się przy tym zwyczajowe numerowanie miesięcy (styczniowi odpowiada jedynka, grudniowi — dwunastka). 2

3

4

5

W przypadku komputerów 32- i 64-bitowych typ int odpowiada 32 bitom (32 b), a więc czterem bajtom (4 B) danych. Trzy liczby w efekcie zajmują w pamięci 3 × 4 = 12 B. Teoretycznie, użytkowo. W praktyce, ze względu na wyrównywanie adresów poszczególnych zmiennych co najmniej do wielokrotności własnego rozmiaru, trzy tak zapisane zmienne nie zajmą 1 + 1 + 4 = 6 B, lecz najczęściej 1 + 1 + 2 + 4 = 8 B (lub więcej). Mimo to stanowi to oszczędność w stosunku do zapisu wykorzystującego trzy liczby całkowite typu int. Pozostawiamy za to zakres od minus dwóch miliardów do plus dwóch miliardów lat (typ signed int), wykazując się znacznie większą dalekowzrocznością niż autorzy oprogramowania sprzed lat, którzy oszczędzali pamięć operacyjną komputera tak dalece, że problemem stało się zapisywanie roku 2000. Należy mieć przy tym nadzieję, że za dwa miliardy lat pisane zgodnie z powyższymi zaleceniami oprogramowanie nie będzie już jednak użytkowane. Większość kompilatorów zapisuje wartości wyliczeniowe jako liczby typu int, zajmujące cztery bajty pamięci niezależnie od liczby opcji wyliczenia.

Rozdział 4.  Struktury i unie

125

Powyższy opis daty jest dokładny i nie pozostaje nic innego, niż wykorzystywać go w programach. Problem polega na tym, że za każdym razem chcąc użyć daty, należałoby zastosować trzy odrębne wartości. Powoduje to zbędne powtarzanie kodu i utrudnia przechowywanie, przekazywanie i obsługę dat. Każde użycie pary opcji kopiuj-wklej w programie należy traktować jako błąd. Programiście nie wolno kopiować kodu w celu wykorzystania go w innym programie lub nawet innym miejscu tego samego programu: każdy powtarzalny fragment powinien zostać zgrupowany i przygotowany do dalszego, powtórnego wykorzystania za pomocą struktur, pętli oraz podprogramów6.

Naturalnym rozwiązaniem jest stworzenie nowego typu danych, który opisuje datę i jest gotowy do wykorzystania wszędzie tam, gdzie zachodzi potrzeba zapamiętania, przekazania lub przetworzenia daty. Taki typ danych w języku C nosi nazwę struktury7.

Deklarowanie struktury Jednokrotne zadeklarowanie nowego typu strukturalnego pozwala na wykorzystywanie tego typu w dalszych częściach tekstu źródłowego programu poprzez odwołanie się do dowolnie wybranej, unikatowej nazwy typu. Deklaracja struktury składa się z następujących elementów:  słowa kluczowego struct (od ang. structure, struktura);  unikatowej nazwy nowego typu złożonego (zgodnej z zasadami nadawania

nazw w języku C);  nawiasu klamrowego otwierającego blok opisu struktury;  listy deklaracji pól składających się na strukturę, zapisanej w sposób zgodny

z zapisem deklaracji zmiennych (nazwa typu danych — nazwa pola — średnik);  nawiasu klamrowego zamykającego blok opisu struktury;  średnika kończącego deklarację. Pominięcie średnika kończącego deklarację struktury stanowi źródło błędów trudnych do zdiagnozowania. Kompilator najczęściej wykazuje błędy w dalszych wierszach programu, co stanowi szczególny problem w przypadku, gdy błędna deklaracja struktury znajduje się w oddzielnym zbiorze nagłówkowym. Ta sytuacja wynika z możliwości definiowania zmiennych strukturalnych (instancji typu złożonego) przez podanie nazw bezpośrednio po nawiasie klamrowym zamykającym blok opisu struktury (a przed średnikiem kończącym deklarację).

Na przykład struktura opisująca datę — przy założeniu, że nowemu złożonemu typowi danych nadana zostanie nazwa Data — przyjmie następującą postać: 6 7

Pętle oraz podprogramy zostały opisane w dalszych rozdziałach książki. W innych językach programowania złożone typy danych noszą często nazwę rekordów.

Część II  Język C

126 enum Miesiace { Styczen = 1, Luty, Marzec, Kwiecien, Maj, Czerwiec, Lipiec, Sierpien, Wrzesien, Pazdziernik, Listopad, Grudzien }; struct Data { unsigned char Dzien; enum Miesiace Miesiac; signed int Rok; };

Od tego momentu struct Data odpowiada nowemu typowi danych, a wpisanie tych dwóch wyrazów oszczędza programiście powtarzanie (lub kopiowanie) definicji trzech zmiennych, w sumie wymagającej wpisania dziewięciu wyrazów i wprowadzającej ryzyko pomyłki.

Definiowanie zmiennej strukturalnej Deklaracja złożonego typu danych stanowi (jak zresztą każda deklaracja) wyłącznie zapowiedź, że taki typ może być wykorzystywany w dalszej części programu. By faktycznie go użyć — a więc zarezerwować miejsce w pamięci na zmienną strukturalną — należy zapisać definicję, np. postaci: struct Data DataUrodzenia, DataImmatrykulacji;

W ten sposób powstają dwie zmienne — DataUrodzenia oraz DataImmatrykulacji — z których każda stanowi pełną reprezentację daty, składającą się z dnia miesiąca, numeru miesiąca (zapisanego wyliczeniem, a więc z możliwością stosowania w tekście źródłowym programu zwyczajowych nazw miesięcy) oraz numeru roku. Za każdym razem gdy używa się wcześniej zadeklarowanego typu złożonego, wykorzystywana jest poprawna, sprawdzona, raz zapisana postać, a jeżeli w pewnym momencie odkryje się błąd w zapisie struktury lub możliwość wprowadzenia pewnej optymalizacji, zmiana od razu dotyczyć będzie wszystkich definicji zmiennych strukturalnych. Tak działa właśnie zasada „napisz raz, korzystaj wielokrotnie”, którą muszą wziąć sobie do serca wszyscy programiści. Podobnie jak zmienne typów prostych zawartość zmiennej strukturalnej zaraz po jej zdefiniowaniu jest całkowicie przypadkowa. Oznacza to również, że wartości poszczególnych zmiennych składowych (czyli pól) struktury mogą wykraczać poza przyjęty dla nich zakres. Na przykład nowo stworzona zmienna typu struct Data może reprezentować 65. dzień 40. miesiąca roku –174 635. Możliwe jest zdefiniowanie zmiennej strukturalnej połączone z narzuceniem początkowych wartości pól tworzonej struktury. Na przykład zmienna typu struct Data może zostać zdefiniowana w następujący sposób: struct Data DataUrodzenia = { 12, Styczen, 2005 };

Kolejność wartości podawanych między nawiasami klamrowymi musi być w takim przypadku zgodna z kolejnością występowania pól w deklaracji struktury.

Rozdział 4.  Struktury i unie

127

Korzystanie z pól struktury Aby odczytać lub zmodyfikować zawartość jednego z pól zmiennej strukturalnej, należy do nazwy tej zmiennej dołączyć kropkę, a następnie wpisać nazwę pola. Na przykład jeżeli zdefiniowana zostanie następująca zmienna typu złożonego struct Data: struct Data DataUrodzenia;

to wpisanie do niej poprawnej daty można osiągnąć w następujący sposób: DataUrodzenia.Dzien = 12; DataUrodzenia.Miesiac = Listopad; DataUrodzenia.Rok = 1997;

a odczytanie np. w celu wyświetlenia daty na ekranie: printf("Data urodzenia osoby: %u-%02u-%02u\n", DataUrodzenia.Rok, DataUrodzenia.Miesiac, DataUrodzenia.Dzien);

Na powyższym przykładzie widać, że typ wyliczeniowy może być przez podprogram printf() interpretowany wyłącznie jako liczba. Nazwy miesięcy są ułatwieniem dla programisty na etapie tworzenia programu, jednak po jego skompilowaniu znikają raz na zawsze z kodu i jedynym, co można uzyskać na podstawie zmiennych wyliczeniowych, są wartości liczbowe skojarzone z poszczególnymi pozycjami wyliczenia. Nie da się za pomocą żadnego podprogramu standardowego, a tym bardziej podprogramu printf(), wypisać na ekranie zawartości całej struktury. Strukturę można wyświetlać tylko pole po polu, ręcznie nakazując wyświetlenie kolejnego pola za pomocą właściwego kodu formatującego oraz podania nazwy pola.

Całość programu zawierającego deklarację typu wyliczeniowego opisującego nazwy miesięcy oraz typu złożonego opisującego datę, a także przykład definicji zmiennej strukturalnej, zapisania jej zawartości i odczytania jej w celu wyświetlenia informacji na ekranie, zawiera listing 4.1. Efekt działania programu przykładowego przedstawiono na rysunku 4.1. Listing 4.1. Przykład zastosowania struktur jako złożonych typów danych #include #include // Tylko Visual Studio enum Miesiace { Styczen = 1, Luty, Marzec, Kwiecien, Maj, Czerwiec, Lipiec, Sierpien, Wrzesien, Pazdziernik, Listopad, Grudzien }; struct Data { unsigned char Dzien; enum Miesiace Miesiac; signed int Rok; }; int main() {

Część II  Język C

128

// Definicja zmiennej typu złożonego. struct Data DataUrodzenia; // Wpisanie nowej daty do zmiennej strukturalnej. DataUrodzenia.Dzien = 12; DataUrodzenia.Miesiac = Listopad; DataUrodzenia.Rok = 1997; // Wypisanie daty na ekranie. printf("Data urodzenia osoby: %u-%02u-%02u\n", DataUrodzenia.Rok, DataUrodzenia.Miesiac, DataUrodzenia.Dzien); // Koniec programu. _getch(); // Tylko Visual Studio return 0; }

Rysunek 4.1. Efekt działania programu z listingu 4.1 Środowisko Microsoft Visual Studio ułatwia korzystanie z typów złożonych. Po naciśnięciu kropki na ekranie pojawia się lista prezentująca wszystkie pola składowe struktury (rysunek 4.2). Aby wstawić do tekstu źródłowego programu nazwę wybranego pola, wystarczy wybrać je klawiszami kursora i nacisnąć klawisz Enter. Alternatywnie można wpisać wystarczającą liczbę znaków nazwy pola, by wybór stał się jednoznaczny, i również nacisnąć Enter.

Rozdział 4.  Struktury i unie

129

Rysunek 4.2. Lista rozwijana, umożliwiająca wprowadzenie nazwy pola do tekstu źródłowego programu bez konieczności jej ręcznego wpisywania

Zagnieżdżanie struktur Każdy typ złożony staje się — wraz z momentem deklaracji — pełnoprawnym typem języka C8. Oznacza to m.in., że można go wykorzystywać również do deklarowania nowych typów złożonych. Na przykład aby stworzyć typ złożony opisujący studenta i przechowujący następujące cztery informacje o nim:  numer albumu (legitymacji),  datę urodzenia,  datę przyjęcia na studia,  datę skreślenia ze studiów,

wystarczy — wykorzystując do tego już zadeklarowane typy opisujące nazwy miesięcy (wyliczenie) oraz datę (struktura) — zapisać następującą deklarację typu Student: struct Student { unsigned int NrAlbumu; struct Data DataUrodzenia; struct Data DataPrzyjecia; struct Data DataSkreslenia; };

Możemy przy tym przyjąć, że nieprawidłowa data (np. zerowy numer roku) odpowiada studentowi, który albo nie został jeszcze przyjęty na studia, albo nie został z nich skreślony. Listing 4.2 przedstawia przykład programu wczytującego do zmiennej strukturalnej typu struct Student informacje z klawiatury, a następuje wyświetlającego je z powrotem na ekranie; na rysunku 4.3 można zaobserwować efekty działania programu. Jako zadanie domowe Czytelnik może potraktować uzupełnienie programu o weryfikację wprowadzanych danych tak, aby niemożliwe było wprowadzenie nieprawidłowego numeru dnia, miesiąca czy roku. Listing 4.2. Przykład zastosowania struktur zagnieżdżonych #include #include // Tylko Visual Studio enum Miesiace 8

Dotyczy to zresztą wszelkich deklaracji typów, w tym deklaracji typów wyliczeniowych, przedstawianych w poprzednim rozdziale.

Część II  Język C

130 { Styczen = 1, Luty, Marzec, Kwiecien, Maj, Czerwiec, Lipiec, Sierpien, Wrzesien, Pazdziernik, Listopad, Grudzien }; struct Data { unsigned char Dzien; enum Miesiace Miesiac; signed int Rok; }; struct Student { unsigned int NrAlbumu; struct Data DataUrodzenia; struct Data DataPrzyjecia; struct Data DataSkreslenia; }; int main() { // Definicja zmiennej typu złożonego oraz innych zmiennych. char Opcja; struct Student DaneStudenta; // Wczytanie numeru albumu. printf("Podaj numer albumu: "); scanf("%u", &DaneStudenta.NrAlbumu); // Wczytanie daty urodzenia. printf("Podaj date urodzenia (dzien miesiac rok): "); scanf("%hhu %i %i", &DaneStudenta.DataUrodzenia.Dzien, &DaneStudenta.DataUrodzenia.Miesiac, &DaneStudenta.DataUrodzenia.Rok); // Opróżnij bufor wejściowy. scanf("%c", &Opcja); // Opcjonalne wczytanie daty przyjęcia na studia. printf("Czy student przyjety na studia (t/n)?"); scanf("%c", &Opcja); if ((Opcja == 't') || (Opcja == 'T')) { printf("Podaj date przyjecia (dzien miesiac rok): "); scanf("%hhu %u %i", &DaneStudenta.DataPrzyjecia.Dzien, &DaneStudenta.DataPrzyjecia.Miesiac, &DaneStudenta.DataPrzyjecia.Rok); // Opróżnij bufor wejściowy. scanf("%c", &Opcja); // Opcjonalne wczytanie daty przyjęcia na studia. printf("Czy student skreslony ze studiow (t/n)?"); scanf("%c", &Opcja); if ((Opcja == 't') || (Opcja == 'T')) { printf("Podaj date skreslenia (dzien miesiac rok): "); scanf("%hhu %u %i", &DaneStudenta.DataUrodzenia.Dzien, &DaneStudenta.DataSkreslenia.Miesiac, &DaneStudenta.DataSkreslenia.Rok); } else { DaneStudenta.DataSkreslenia.Rok = 0; } } else { DaneStudenta.DataPrzyjecia.Rok = 0; DaneStudenta.DataSkreslenia.Rok = 0;

Rozdział 4.  Struktury i unie

131

} // Wypisanie danych studenta na ekranie. printf("\nNumer albumu: %u\n", DaneStudenta.NrAlbumu); printf("Data urodzenia: %u-%02u-%02u\n", DaneStudenta.DataUrodzenia.Rok, DaneStudenta.DataUrodzenia.Miesiac, DaneStudenta.DataUrodzenia.Dzien); if (DaneStudenta.DataPrzyjecia.Rok != 0) { printf("Data przyjecia na studia: %u-%02u-%02u\n", DaneStudenta.DataPrzyjecia.Rok, DaneStudenta.DataPrzyjecia.Miesiac, DaneStudenta.DataPrzyjecia.Dzien); if (DaneStudenta.DataSkreslenia.Rok != 0) printf("Data skreslenia ze studiow: %u-%02u-%02u\n", DaneStudenta.DataSkreslenia.Rok, DaneStudenta.DataSkreslenia.Miesiac, DaneStudenta.DataSkreslenia.Dzien); else printf("Data skreslenia ze studiow: (brak)\n"); } else { printf("Data przyjecia na studia: (brak)\n" "Data skreslenia ze studiow: (brak)\n"); } // Koniec programu. _getch(); // Tylko Visual Studio return 0; }

Rysunek 4.3. Efekt działania programu z listingu 4.2

W programie przedstawionym na listingu 4.2 zastosowane zostały następujące fragmenty kodu, które mogą być przy pierwszym spojrzeniu mało zrozumiałe: 1. W poniższym fragmencie: // Wczytanie daty urodzenia. printf("Podaj date urodzenia (dzien miesiac rok): "); scanf("%hhu %i %i", &DaneStudenta.DataUrodzenia.Dzien, &DaneStudenta.DataUrodzenia.Miesiac, &DaneStudenta.DataUrodzenia.Rok);

numer miesiąca jest wczytywany bezpośrednio do struktury jako liczba typu signed int, mimo że pole DaneStudenta.DataUrodzenia.Miesiac jest typu wyliczeniowego. Korzystamy tutaj z wiedzy z poprzedniego rozdziału, że wartości wyliczeń są wewnętrznie zapisywane jako liczby typu int. 2. Fragment: // Opróżnij bufor wejściowy. scanf("%c", &Opcja);

Część II  Język C

132

jest wymagany ze względu na wczytywanie w kolejnym kroku pojedynczego znaku do zmiennej typu char (kod formatujący %c). Ponieważ wprowadzenie poprzedniej porcji informacji z klawiatury zostało zatwierdzone naciśnięciem przez użytkownika klawisza Enter, do zmiennej char zostałby wczytany nie kolejny znak, ale właśnie pozostawiony w buforze klawiatury kod klawisza Enter. Dodatkowym, zbędnym z punktu widzenia logiki programu wywołaniem podprogramu scanf() odczytuje się ten kod z bufora, pozbywając się go przed właściwym wczytywaniem znaku. Brak tej instrukcji spowodowałby, że program nie zatrzymywałby się w oczekiwaniu na naciśnięcie klawiszy t lub n, lecz od razu wczytywał kod naciśniętego klawisza 10 (i w efekcie reagował jak na naciśnięcie klawisza n lub dowolnego innego). 3. Fragmenty kodu wykonywane w przypadku, gdy student nie został w ogóle

przyjęty na studia lub nie został z nich jeszcze skreślony: DaneStudenta.DataPrzyjecia.Rok = 0; DaneStudenta.DataSkreslenia.Rok = 0;

wpisują do pól określających datę przyjęcia i/lub skreślenia studenta tylko rok równy zero, pozostawiając bez zmian przypadkowe wartości numeru dnia i miesiąca. Nie jest to oczywiście najbardziej eleganckie rozwiązanie, jednak w tym przypadku jest najzupełniej wystarczające, gdyż w przypadku niepoprawnej daty analizowany jest tylko numer roku: jeżeli jest on równy zero, numery dnia i miesiąca są ignorowane. W ten sposób oszczędza się wpisywania co najmniej czterech zbędnych wierszy tekstu źródłowego programu. Powyższy fragment mógłby zostać zapisany również w poniższy, oszczędniejszy (choć niekoniecznie tak samo czytelny) sposób: DaneStudenta.DataPrzyjecia.Rok = DaneStudenta.DataSkreslenia.Rok = 0;

Czyszczenie bufora wejściowego klawiatury poprzez usunięcie z niego kodu wcześniej naciśniętego klawisza Enter można by uprościć, zmieniając fragmenty tekstu źródłowego programu o postaci: // Opróżnij bufor wejściowy. scanf("%c", &Opcja); // Opcjonalne wczytanie daty przyjęcia na studia. printf("Czy student przyjety na studia (t/n)?"); scanf("%c", &Opcja);

na następujące, przy założeniu wcześniejszego zdefiniowania dodatkowej zmiennej pomocniczej char Enter, przeznaczonej do odbierania zbędnego kodu z bufora klawiatury: // Opcjonalne wczytanie daty przyjęcia na studia. printf("Czy student przyjety na studia (t/n)?"); scanf("%c%c", &Enter, &Opcja);

Rozdział 4.  Struktury i unie

133

Kopiowanie zmiennych strukturalnych Jedną z zalet stosowania zmiennych strukturalnych jest możliwość wyrażenia operacji skopiowania stanu jednej zmiennej typu złożonego do innej zmiennej typu złożonego za pomocą pojedynczej instrukcji. Na przykład zamiast kopiować datę ze zmiennej A do zmiennej B w następujący sposób: struct Data A, B; A.Dzien = 10; A.Miesiac = Luty; A.Rok = 1990; B.Dzien = A.Dzien; B.Miesiac = A.Miesiac; B.Rok = A.Rok;

można po prostu nakazać przepisanie całej zawartości struktury A do struktury B: struct Data A, B; A.Dzien = 10; A.Miesiac = Luty; A.Rok = 1990; B = A;

W ten sposób można przy pisaniu programów uniknąć trzech kategorii błędów:  pominięcia kopii niektórych pól typu złożonego, np.: B.Dzien = A.Dzien; B.Rok = A.Rok; // BŁĄD! Zapomniano o skopiowaniu numeru miesiąca!

 skopiowania niewłaściwego pola struktury, np.: B.Dzien = A.Dzien; B.Miesiac = A.Miesiac; B.Rok = A.Dzien; // BŁĄD! Numerem roku staje się numer dnia!

 pomylenia kierunku kopiowania jednego z pól, np.: B.Dzien = A.Dzien; A.Miesiac = B.Miesiac; // BŁĄD! Zamiast skopiować pole niszczy się oryginał! B.Rok = A.Rok;

Wszystkie powyżej wymienione kategorie błędów wydają się — w perspektywie wykorzystywanej tu jako przykład struktury opisującej datę — trudne do popełnienia. Jednak w przypadku struktur danych składających się z kilkunastu lub kilkudziesięciu pól, a szczególnie gdy do powielania dużych fragmentów tekstu źródłowego programu kopiującego poszczególne pola wykorzystuje się szkodliwą technikę kopiuj-wklej, popełnienie jednego z tych błędów staje się całkiem prawdopodobne. Z tego powodu warto przechodzić przy programowaniu na wyższe poziomy abstrakcji i kopiować struktury nie pole po polu, lecz całościowo.

Część II  Język C

134

Unie We wszystkich dotychczas występujących przykładach definicja zmiennej oznaczała zarezerwowanie fragmentu pamięci operacyjnej komputera na cele przechowywania wartości tej zmiennej. Tak zarezerwowany wycinek pamięci opatrywany był nazwą (nazwą zmiennej) i stanowił jakby wyłączną własność tej zmiennej: żadna inna zmienna nie miała prawa korzystać z tej pamięci9. Czasem jednak wyłączne wykorzystywanie pamięci jest całkowicie niepotrzebne. Wyobraźmy sobie strukturę danych pozwalającą przechowywać stan magazynowy produktu albo w formie liczby naturalnej plus zero, albo zmiennoprzecinkowej typu double: enum TypLiczebnosci { LiczbaNaturalna, LiczbaZmiennoprzecinkowa }; struct StanMagazynowy { enum TypLiczebnosci typ; unsigned int naturalny; double zmiennoprzecinkowy; }; struct StanMagazynowy A, B; A.typ = LiczbaNaturalna; A.naturalny = 10; B.typ = LiczbaZmiennoprzecinkowa; B.zmiennoprzecinkowy = 12.34;

Tak wyrażony stan magazynowy nigdy nie będzie wyrażany jednocześnie liczbą naturalną i zmiennoprzecinkową: jedna opcja wyklucza drugą. Tymczasem struktura struct StanMagazynowy zajmie 16 B pamięci operacyjnej: 4 B na pole typ, 4 B na pole naturalny i 8 B na pole zmiennoprzecinkowy. A gdyby tak zapisać liczebność naturalną lub ilość zmiennoprzecinkową w tym samym miejscu pamięci? Tutaj wkraczają unie. Unia jest złożonym typem danych pozwalającym na wskazanie pól, które mają współdzielić miejsce w pamięci operacyjnej, przez co stają się dostępne zamiennie: jeżeli zapisze się coś do wybranego pola, nie można odczytywać pozostałych, dopóki nie zmieni się zdania i nie dokona zapisu do innego pola. Oczywiście, nic nie stoi na przeszkodzie, by dokonywać odczytów niewłaściwych pól, jednak efekt jest taki sam, jak we wszystkich innych podobnych sytuacjach w języku C: odczyta się wartości niepoprawne, sprawiające wrażenie losowych.

9

Nie jest to do końca prawdą. Za pomocą zmiennych wskaźnikowych można wniknąć do wnętrza innych zmiennych, odczytać ich zawartość na dowolny sposób (całościowo lub wycinkami), a także wprowadzić dowolne zmiany, również niezgodne z typem danej zmiennej.

Rozdział 4.  Struktury i unie

135

Deklarowanie unii Deklaracja unii składa się z następujących elementów:  słowa kluczowego union;  unikatowej nazwy nowego typu złożonego (zgodnej z zasadami nadawania

nazw w języku C);  nawiasu klamrowego otwierającego blok opisu unii;  listy deklaracji pól składających się na unię, zapisanej w sposób zgodny

z zapisem deklaracji zmiennych (nazwa typu danych — nazwa pola — średnik);  nawiasu klamrowego zamykającego blok opisu unii;  średnika kończącego deklarację.

Wszystkie pola zapisane wewnątrz nawiasów klamrowych — niezależnie od ich liczby — współdzielą ten sam obszar pamięci. Unia zajmuje w pamięci tylko tyle miejsca, ile największy z jej składników. Nie jest przy tym w żaden sposób zapisywana informacja, które z pól zawiera w danym momencie poprawną wartość: pamiętanie o tym (poprzez właściwe zapisywanie tekstu źródłowego programu) jest obowiązkiem programisty. Na przykład unia umożliwiająca przechowanie w jednym bloku pamięci liczby naturalnej (plus zero) oraz zmiennoprzecinkowej (typu double) może wyglądać następująco: union NZ { unsigned int naturalne; double zmiennoprzecinkowe; };

Definiowanie unii Aby stworzyć zmienną stanowiącą unię, wystarczy użyć słowa kluczowego union, podać nazwę złożonego typu danych będącego unią, a następnie dowolną nazwę zmiennej, zgodnie z konwencją nazywania zmiennych obowiązującą w języku C: union NZ { unsigned int naturalne; double zmiennoprzecinkowe; }; union NZ Liczebnosc;

Korzystanie z pól unii Unie wykorzystuje się podobnie jak struktury: podając. Jedyną różnicą jest to, że o ile w przypadku struktur wszystkie pola są cały czas do dyspozycji programisty, o tyle w przypadku unii poprawną wartość przechowuje tylko to jedno pole, do którego jako ostatni został wykonany zapis nowej poprawnej wartości.

Część II  Język C

136

Program prezentujący ten problem został przedstawiony na listingu 4.3. Do zmiennej będącej instancją zaprezentowanej powyżej unii typu union NZ zapisywane są w nim dwie wartości, raz — naturalna, a raz — zmiennoprzecinkowa. Efekt działania programu przedstawiono na rysunku 4.4. Jak widać, po zapisaniu pola naturalnego pole zmiennoprzecinkowe zawiera wartość o charakterze przypadkowym i odwrotnie — zapisanie pola zmiennoprzecinkowego powoduje, że tym razem pole naturalne przybiera wartość wydającą się przypadkową10. Listing 4.3. Przykład zastosowania unii jako złożonych typów danych #include #include // Tylko Visual Studio union NZ { unsigned int naturalne; double zmiennoprzecinkowe; }; int main() { // Definicje zmiennych. union NZ Liczebnosc; // Zapis pola naturalnego. Liczebnosc.naturalne = 12345; printf("Liczebnosc.naturalne = printf("Liczebnosc.zmiennoprzecinkowe = // Zapis pola zmiennoprzecinkowego. Liczebnosc.zmiennoprzecinkowe = 12.345; printf("Liczebnosc.naturalne = printf("Liczebnosc.zmiennoprzecinkowe = // Koniec programu. _getch(); // Tylko Visual Studio return 0; }

%u\n", Liczebnosc.naturalne); %f\n", Liczebnosc.zmiennoprzecinkowe); %u\n", Liczebnosc.naturalne); %f\n", Liczebnosc.zmiennoprzecinkowe);

Rysunek 4.4. Efekt działania programu z listingu 4.3

Unie anonimowe w strukturach Tworzenie osobnego złożonego typu danych stanowiącego unię stosuje się dość rzadko. Najczęściej unie występują jako pola struktur. W takim przypadku nie ma potrzeby nadawania nazwy nowemu typowi: wystarczy stworzyć unię anonimową, po czym polu przez nią tworzonemu nadać nazwę, która jednak nie nazywa już typu, a wyłącznie pole.

10

Tak naprawdę te wartości nie są przypadkowe, tylko wynikają z binarnego zapisu wartości naturalnych oraz zmiennoprzecinkowych. Gdyby obydwa pola były podobnych typów — np. unsigned char oraz signed int — w pewnym zakresie wartości zmiana jednego pola powodowałaby lustrzaną zmianę wartości drugiego pola.

Rozdział 4.  Struktury i unie

137

Pokazuje to, dlaczego pominięcie średnika na końcu deklaracji unii11 powoduje pojawienie się tak dziwnych (i często trudnych do zdiagnozowania) błędów podczas kompilacji programu: pomiędzy klamrą kończącą blok opisu pól unii a średnikiem kończącym deklarację typu można wpisać nazwę zmiennej lub pola danego typu złożonego. Przedstawioną na początku tego podrozdziału strukturę opisującą stan magazynowy można z wykorzystaniem unii anonimowej zapisać następująco: enum TypLiczebnosci { LiczbaNaturalna, LiczbaZmiennoprzecinkowa }; struct StanMagazynowy { enum TypLiczebnosci typ; union { unsigned int naturalny; double zmiennoprzecinkowy; } liczebnosc; };

Program demonstrujący zastosowanie unii do zapisywania stanu magazynowego został przedstawiony na listingu 4.4. Dokonuje on wczytania z klawiatury rodzaju stanu magazynowego (naturalny lub zmiennoprzecinkowy) oraz samej wartości stanu, a następnie wyświetlenia tego stanu na ekranie. Oprócz unii zastosowana jest tutaj również struktura, która — choć w tym przypadku niepotrzebna — może zostać zastosowana w dalszych programach Czytelnika jako budulec większych programów. Efekt działania programu został przedstawiony na rysunku 4.5. Listing 4.4. Program demonstrujący praktyczne zastosowanie unii anonimowych jako pól struktur #include #include // Tylko Visual Studio enum TypLiczebnosci { LiczbaNaturalna, LiczbaZmiennoprzecinkowa }; struct StanMagazynowy { enum TypLiczebnosci typ; union { unsigned int naturalne; double zmiennoprzecinkowe; } liczebnosc; }; int main() { // Definicje zmiennych. char typ; struct StanMagazynowy stan; // Wczytanie typu liczebności. printf("Podaj rodzaj liczebnosci:\n" " n - naturalna\n" " z - zmiennoprzecinkowa\n"); scanf("%c", &typ);

11

Podobnie zresztą jak w przypadku struktur.

Część II  Język C

138

// Wczytanie samej liczebności. switch (typ) { case 'n': stan.typ = LiczbaNaturalna; printf("Podaj liczebnosc naturalna: "); scanf("%u", &stan.liczebnosc.naturalne); break; case 'z': stan.typ = LiczbaZmiennoprzecinkowa; printf("Podaj liczebnosc zmiennoprzecinkowa: "); scanf("%lf", &stan.liczebnosc.zmiennoprzecinkowe); break; default: printf("Nieznany typ liczebnosci, przerywam prace!\n"); return 0; } // Wypisz typ liczebności oraz liczebność. printf("Stan magazynowy przechowuje liczebnosc w zmiennej typu %s.\nStan = ", (stan.typ == LiczbaNaturalna ? "naturalnego" : "zmiennoprzecinkowego")); if (stan.typ == LiczbaNaturalna) printf("%u\n", stan.liczebnosc.naturalne); else printf("%lf\n", stan.liczebnosc.zmiennoprzecinkowe); // Koniec programu. _getch(); // Tylko Visual Studio return 0; }

Rysunek 4.5. Efekt działania programu z listingu 4.4 Niejako przy okazji zaprezentowane zostały też inne elementy języka C, w zasadzie zawsze wykorzystywane łącznie z uniami: instrukcja warunkowa if, wyrażenie warunkowe znaku zapytania oraz konstrukcja warunkowa switch.

Podsumowanie Złożone typy danych pozwalają osiągnąć dwa ważne cele skutecznego programowania:  Ograniczyć rozmiar tekstu źródłowego programu przez unikanie zbędnych

powtórzeń.  Zmniejszyć zajętość pamięci operacyjnej na potrzeby danych wykorzystywanych

przez program przez zapisywanie informacji nigdy nie wykorzystywanych jednocześnie we wspólnym bloku pamięci.

Rozdział 4.  Struktury i unie

139

Oto podsumowanie najważniejszych zagadnień przedstawionych w tym rozdziale:  Złożone typy danych są deklarowane przez programistę i mogą być

wykorzystywane w dalszej części tekstu źródłowego programu tak samo, jak typy proste.  Struktury grupują dowolną liczbę pól dowolnych typów, upraszczając

definiowanie zmiennych określających obiekty fizyczne o rozbudowanym opisie i ograniczając ilość tekstu źródłowego potrzebnego do opisania regularnie wykorzystywanych zbiorów powiązanych ze sobą danych.  Struktury mogą zawierać inne struktury, co pozwala na tworzenie wysoce

hierarchicznych struktur danych, opisujących bardzo złożone obiekty fizyczne.  Struktury można kopiować jedną instrukcją, zapobiegając wielu błędom

i ograniczając jeszcze bardziej rozmiar tekstu źródłowego programu.  Unie umożliwiają współdzielenie pamięci przez kilka pól różnych typów,

dzięki czemu można oszczędniej gospodarować pamięcią, gdy naraz potrzebne jest przechowywanie tylko jednej z kilku różnych informacji.  Unie anonimowe można wykorzystać wewnątrz struktur, wiążąc w ten sposób

potrzebne informacje zawsze z tymi, które potrzebne są naprzemiennie i zapisywane są w postaci unii.  Poprawną wartość zawiera tylko pole unii, do którego jako ostatni był

realizowany zapis informacji — pozostałe pola zawierają wartości o charakterze przypadkowym, wynikające z różnic w zapisie różnego typu informacji.  Korzystanie z unii najczęściej wiąże się ze stosowaniem instrukcji (if), wyrażeń (?:) i konstrukcji (switch) warunkowych, umożliwiających realizację

różnych fragmentów kodu w zależności od tego, które pole unii przechowuje poprawną wartość.

140

Część II  Język C

Rozdział 5.

Pętle, tablice i zmienne łańcuchowe Programista podczas pracy musi pamiętać, że celem programowania jest zrzucenie jak największej części pracy na komputer. Szczególną kategorią zadań, z którymi ludzie radzą sobie bardzo źle, szybko się nużąc i nudząc, a które komputery realizują doskonale, wydajnie, bez protestów i zawsze tak samo dokładnie, są wszelkie powtarzalne operacje. Znakomita większość zadań związanych z przetwarzaniem informacji dotyczy zresztą właśnie powtarzania tej samej operacji kilka, kilkanaście, kilka tysięcy lub kilkanaście milionów razy. Ponieważ kopiowanie tych samych fragmentów tekstu źródłowego programu tysiące razy w celu wielokrotnego wykonania operacji nie wchodzi w rachubę1, należy wykorzystać możliwości języka programowania dotyczące wielokrotnego realizowania określonych fragmentów programu. Również przechowywanie większych zbiorów danych tego samego typu jest nie do zrealizowania za pomocą zwykłych zmiennych. W jednej zmiennej można zapisać np. jedną ocenę za kartkówkę nadaną uczniowi — jak jednak zapisać oceny tysiąca uczniów szkoły z jednej kartkówki? Z pomocą przychodzi mechanizm tablic pozwalających zarezerwować większe bloki pamięci na przechowywanie obszerniejszych, lecz wciąż jednolitych zbiorów danych. Skoro za pomocą tablic można tworzyć zbiory wielu elementów tego samego typu, to jeżeli stworzy się tablicę przechowującą znaki, można zapisać nie tylko pojedyncze litery, ale i całe słowa, zdania czy nawet dłuższe fragmenty tekstu. Do operowania na tak zapisanym tekście jest potrzebny zbiór specjalizowanych podprogramów; łatwo też niestety o wprowadzenie do programu błędu2. Przy odrobinie ostrożności i wiedzy uzyskuje się jednak spotykane w niewielu innych językach programowania możliwości szybkiego operowania na wielkich blokach tekstu i dokonywania w nich zmian lub analiz sięgających nawet pojedynczych liter. 1

2

Zresztą — co wielokrotnie było podkreślane w poprzednich rozdziałach tej książki — skorzystanie z opcji kopiuj-wklej w przypadku tekstu źródłowego programu należy traktować jako błąd. Jeżeli jakąś operację trzeba powtórzyć choćby dwukrotnie, należy nakazać komputerowi powtórzenie, a nie duplikować kod. Obydwie wady usuwa rozbudowany, obiektowy typ łańcuchowy obecny w języku C++.

Część II  Język C

142

W tym rozdziale opisane zostaną następujące elementy usprawniające implementację powtarzalnych operacji na dużych zbiorach danych:  pętle — konstrukcje języka umożliwiające wydzielenie instrukcji, która ma być

wykonywana wielokrotnie (lub bloku instrukcji wykonywanego wielokrotnie), i określenie warunku, pod którym mają być realizowane kolejne powtórzenia;  tablice — struktury danych umożliwiające zgromadzenie pod jedną nazwą

zmiennej dowolnej, z góry określonej liczby elementów tego samego typu (prostego lub złożonego), lecz o dowolnych, różnych wartościach;  zmienne łańcuchowe — odmiana tablic służąca przechowywaniu ciągów

(łańcuchów) liter, składających się w sumie na wyrazy, frazy, zdania lub całe obszerne bloki tekstu.

Pętle Początkujący programista, któremu zleci się napisanie instrukcji sumującej wszystkie liczby naturalne z zakresu od 1 do 5 ze skokiem co dwa, napisze następujący wiersz kodu: unsigned int Suma = 1 + 3 + 5;

Jeżeli zadanie rozszerzy się na wszystkie liczby naturalne z zakresu od 1 do 30 ze skokiem co trzy, po chwili nużącej, powtarzalnej pracy stworzy on następujący tekst źródłowy programu: unsigned int Suma = 1 + 4 + 7 + 10 + 13 + 16 + 19 + 22 + 25 + 28;

Jednak po otrzymaniu zadania policzenia sumy wszystkich liczb naturalnych z przedziału od 100 do 100 000 ze skokiem co siedem taki początkujący programista prawdopodobnie zrezygnuje z jego wykonania, zacznie szukać rozwiązania za pomocą wyszukiwarki internetowej lub — dopiero teraz — zacznie wertować podręczniki w poszukiwaniu informacji na temat pętli. Pętle to struktury umożliwiające wielokrotne wykonywanie tej samej instrukcji (lub bloku instrukcji). Podstawowym elementem każdej pętli jest wyrażenie logiczne określające, czy powtarzana instrukcja ma być wykonana jeszcze raz (czyli: czy ma nastąpić kolejny przebieg pętli), czy też pętla powinna zostać zakończona (przerwana), a działanie programu kontynuowane od następnej za pętlą instrukcji. W zależności od miejsca sprawdzania warunku (na początku czy na końcu bloku instrukcji stanowiącego pętlę) oraz od samego warunku liczba przebiegów pętli może być bardzo różna:  jeżeli warunek sprawdzany jest na początku pętli i nie jest spełniony (wartość wyrażenia logicznego równa false), pętla nie zostanie wykonana ani razu;  w większości przypadków liczba przebiegów pętli będzie równa co najmniej 1;

Rozdział 5.  Pętle, tablice i zmienne łańcuchowe

143

 jeżeli warunek zostanie źle sformułowany lub jego wartość zawsze będzie równa true, pętla będzie pętlą nieskończoną i nigdy nie zostanie zakończona;

w praktyce programistycznej tego typu pętle stosuje się dość rzadko — w zwykłym programie wykonanie pętli nieskończonej oznacza najczęściej zawieszenie się programu, czyli pozorne zatrzymanie się w jednym miejscu (pozorne, gdyż program tak naprawdę wciąż działa, tylko nie może opuścić pętli i przejść do wykonywania dalszych instrukcji). Doświadczony programista, któremu przydzieli się zadanie wymienione na początku tego podpunktu, zamiast prostego wyrażenia sumującego 1, 3 i 5 napisze: unsigned int Suma = 0; for (unsigned int i = 1; i