Kurs: Kurs wstępu do programowania KONKURS

Lekcja: Wczytywanie, wypisywanie, zmienne

Część programistyczna: Zmienne i wczytywanie

W części programistycznej wprowadzimy do naszych programów elementy interaktywne.


Zobacz tekst nagrania

Kolejność wykonywania działań w C++ jest taka sama jak w matematyce. Dodawanie i odejmowanie mają taką samą ważność, tak więc działania te są wykonywane od lewej do prawej. Mnożenie i oba typy dzielenia również mają taką samą ważność, więc także są wykonywane od lewej do prawej. Natomiast mnożenie i oba typy dzielenia są wykonywane przed dodawaniem i odejmowaniem. Jeśli chcemy uzyskać inną kolejność wykonywania działań, możemy stosować nawiasy. C++ dopuszcza w wyrażeniach tylko nawiasy okrągłe. Przykładowo, program:

#include <iostream>
using namespace std;
 
int main() {
    cout << 1 + 2 * 3 << endl;
    cout << (1 + 2) * 3 << endl;
}
wypisuje liczby 7 i 9.

Styl programów

Należy wiedzieć o kilku ograniczeniach dotyczących nazw zmiennych. Nazwa zmiennej może składać się z małych i wielkich liter, cyfr oraz znaku podkreślenia _, przy czym nie może zaczynać się od cyfry. W przeciwieństwie do niektórych języków programowania, w nazwach zmiennych rozróżniane są małe i wielkie litery – właściwie w nazwach zmiennych polecamy w ogóle nie używać wielkich liter. Deklaracja zmiennej może zostać umieszczona w programie w dowolnym miejscu przed miejscem, w którym chcemy ze zmiennej skorzystać (czyli np. wczytać lub wypisać jej wartość). Nazwy zmiennych nie mogą się powtarzać (wyjątki od tego ostatniego stwierdzenia przedstawimy później).

Poniższy program oblicza pole i obwód prostokąta dokładnie tak samo jak poprzedni. Jest on jednak istotnie mniej czytelny.

#include <iostream>
using namespace std;
 
int main() {
    int pierwszy_bok,Boknr2;
cin >> pierwszy_bok >> Boknr2;
  cout << "Pole: " << pierwszy_bok*Boknr2
<< " obwod: " << 2*(pierwszy_bok+Boknr2) << endl;
}
Dobrze jednak dbać o to, by Twoje programy wyglądały jednolicie – wtedy za kilka dni łatwiej będzie Ci do nich powrócić. W tym kursie przyjęliśmy kilka powszechnie używanych konwencji, które pomagają zwiększyć czytelność kodu źródłowego, m.in.:
  • każdy operator matematyczny (dodawanie, odejmowanie itp.) jest otoczony z obu stron pojedynczymi spacjami,
  • po każdym przecinku jest spacja,
  • na końcach wierszy nie ma dodatkowych spacji,
  • każdy wiersz funkcji main() jest wcięty w prawo na taki sam odstęp (to pozwala wyróżnić w kodzie zawartość tej głównej funkcji).
Więcej tego typu konwencji pojawi się przy poznawaniu kolejnych elementów języka C++, jednak zazwyczaj nie będziemy o nich mówić wprost – po prostu będziemy je konsekwentnie stosować.

Teraz powiemy jeszcze tylko o jednym ważnym drobiazgu – o komentarzach. Komentarze są to fragmenty kodu źródłowego, które kompilator zupełnie pomija. Ich celem jest zazwyczaj objaśnianie pewnych fragmentów kodu (dla siebie lub dla innych czytelników kodu) albo tymczasowe usuwanie (wykomentowywanie) pewnych fragmentów kodu, których w danym momencie nie chcemy jeszcze na trwałe usuwać, ale chcemy, żeby nie były one aktywne. W C++ są dwa typy komentarzy, które wyglądają tak:

#include <iostream>
using namespace std;
 
int main() {
    int a, b; // kompilator zignoruje wszystko do konca wiersza
    cin >> a >> b; /* a taki komentarz ...
    moze zajmowac wiele wierszy, pod warunkiem, ze
    zamknie sie znakami: */
    cout << "Iloraz: " << a / b << " reszta: " << a % b << endl;
} //tu nie musi byc spacji, ale ze spacja ladnie wyglada
Im dłuższe programy, tym częściej będziemy stosować komentarze.

Zakres typu

Typ int pozwala przechowywać tylko liczby całkowite z ograniczonego zakresu: od \(-2\,147\,483\,648\) do \(2\,147\,483\,647\), czyli mniej więcej od minus dwóch miliardów do plus dwóch miliardów. Jest to tzw. zakres typu. Próba umieszczenia w zmiennej tego typu liczby spoza zakresu (np. poprzez wczytanie czy w wyniku obliczeń) spowoduje błąd przekroczenia zakresu. Wówczas wynik będzie niepoprawny.

Ma to szczególne znaczenie, gdy w programie dodajemy dosyć duże liczby lub wykonujemy mnożenia. Przykładowo, jeśli uruchomimy program obliczający pole prostokąta i jako długości boków podamy \(50\,000\), otrzymamy kompletnie błędne pole prostokąta:

Pole: -1794967296 obwod: 200000
Dla wygody programisty, w języku C++ wprowadzono kilka typów całkowitych, różniących się zakresami. Poniżej umieszczamy listę tych typów. Na tym etapie nie musisz ich próbować zapamiętać – w początkowych lekcjach typ int będzie dla nas w zupełności wystarczający.

typ zakres
short (pełna nazwa: short int) \([-32\,768,32\,767]\)
int, long (pełna nazwa: long int) \([-2\,147\,483\,648,2\,147\,483\,647]\)
long long (pełna nazwa: long long int) mniej więcej \([-10^{19},10^{19}]\)
unsigned short (pełna nazwa: unsigned short int) \([0,65\,536]\)
unsigned int, unsigned long (pełna nazwa: unsigned long int) \([0,4\,294\,967\,296]\)
unsigned long long (pełna nazwa: unsigned long long int) mniej więcej \([0,2 \cdot 10^{19}]\)

Więcej na temat tego, skąd się wzięły takie właśnie typy i ich zakresy, znajdziesz w komentarzu do lekcji.

Uwaga: Są inne kompilatory języka C++ (z których w tym kursie nie będziemy korzystać), w których typ int ma tylko zakres typu short int. Aby mieć absolutną pewność co do zakresu typu, można zamiast typu int używać typu long int.

Komentarz: Bit i bajt

Dowiedzieliśmy się już, że typy całkowite nie reprezentują wszystkich liczb całkowitych. Więcej, liczby te mogą być reprezentowane przez różne typy o specyficznych zakresach reprezentacji. Dlaczego tak jest?

Do zapisywania liczb w powszechnie używanym układzie dziesiętnym używamy dziesięciu cyfr. W komputerze, z przyczyn technologicznych, do zapisywania informacji używamy dwóch cyfr (znaków): 0 (zera) i 1 (jedynki). Zauważmy, że 1 i 0 możemy interpretować odpowiednio jako jest i nie ma, prawda i fałsz, białe i czarne lub włączony i wyłączony. Mając tylko dwie wartości, można przekazać elementarną informację o stanie pewnego obiektu. Przyjęto, że najmniejszą jednostką informacji jest bit, który może przyjmować jedną z dwóch wartości.

W komputerze liczby całkowite są reprezentowane w postaci ciągów bitów, przy czym dla ustalonego języka programowania jest też ustalona długość takiego ciągu. Może być czasem kilka różnych ustalonych długości. Na przykład w języku C++ mamy kilka różnych typów dla liczb całkowitych, tj. short int, long int i long long int oraz wersje tych typów dla liczb nieujemnych (z przedrostkiem unsigned).

Reprezentacja liczb całkowitych nieujemnych

Najpierw opiszemy, w jaki sposób są reprezentowane liczby nieujemne. Niech długością reprezentacji będzie \(k\). Będziemy mieli zatem bity jak poniżej (tak zapisujemy też liczby w układzie dziesiętnym – najbardziej znacząca cyfra jest z lewej strony, a najmniej znacząca z prawej strony):

\(b_{k-1}b_{k-2}b_{k-3}\ldots b_3b_2b_1b_0\),

a reprezentowana liczba będzie miała wartość:

\(b_0 \cdot 2^0 + b_1 \cdot 2^1 + b_2 \cdot 2^2 + \ldots + b_{k-2} \cdot 2^{k-2} + b_{k-1} \cdot 2^{k-1}\).

Najmniejszą z reprezentowanych liczb jest 0 (same bity 0), a największą \(2^k-1\) (same bity 1). Przedział \([0,2^k-1]\) (lub \([0,2^k)\)) nazywa się zakresem reprezentacji. Typowe wartości \(k\) to 8, 16, 32, 64. Zapiszmy w tabelce odpowiadające im zakresy:

 \(k\)  8  16  32  64
 zakres   \(256\)   \(65\,536\)   \(\approx 4 \cdot 10^9\)   \(10^{19}\) 

Długość 8 jest szczególna, jako rozsądnie możliwie najkrótsza, i nazywa się bajt (ang. byte). Jednak reprezentacja o długości 8 jest stosowana rzadko. Najczęściej długością reprezentacji jest 16 (dwubajtowy typ short int), 32 (czterobajtowy typ long int) lub nawet 64 (ośmiobajtowy typ long long int).

Operacje \(+\), \(-\), \(*\) i \(/\) na liczbach całkowitych zapisanych dwójkowo wykonuje się według prostych algorytmów działań pisemnych znanych ze szkoły (czasem trochę przyśpieszonych przez zastosowanie sprytnych pomysłów). Przykładowo, dodawanie \(100+86\) zapisane dwójkowo wygląda tak (zauważ, że w przypadku dodawania dwóch jedynek powstaje cyfra – tj. bit – przeniesienia):

 7   6   5   4   3   2   1   0   numer bitu
 0  1  1  0  0  1  0  0  (\(100=2^6+2^5+2^2\)) 
 +   0  1  0  1  0  1  1  0  (\(86=2^6+2^4+2^2+2^1\)) 
 1  0  1  1  1  0  1  0  (\(186=2^7+2^5+2^4+2^3+2^1\)) 

Należy jednak pamiętać, że wynik może być nieokreślony (dzielenie przez 0), ale może także wyjść poza zakres (wtedy też jest w pewnym sensie nieokreślony). Gdy w arytmetyce jednobajtowej wykonamy dodawanie \(129+129\), to otrzymamy:

 8   7   6   5   4   3   2   1   0   numer bitu 
 1  0  0  0  0  0  0  1  (129)
 +   1  0  0  0  0  0  0  1  (129)
 1  0  0  0  0  0  0  1  0  (258)

czyli wynik wychodzący poza zakres. Objawia się to bitem na pozycji 8 równym 1.

Jeśli interesuje Cię, jak komputer radzi sobie z takimi sytuacjami, to jest to tak, że bity niemieszczące się w zakresie zostają utracone – czyli wynikiem powyższego działania w typie jednobajtowym byłoby po prostu 2. Odpowiada to obliczeniu reszty z dzielenia wyniku przez \(2^k\).

Reprezentacja liczb ujemnych

Liczby całkowite można reprezentować, wyróżniając jakiś bit, na przykład pierwszy od lewej strony w słowie, i traktując go jako znak liczby. Gdy bit ten równa się 0, liczba jest nieujemna, gdy równa się 1, liczba jest ujemna. Taki rodzaj reprezentacji nazywa się znak-moduł. Ta reprezentacja wyszła zupełnie z użycia, bowiem operacje arytmetyczne realizuje się w niej niewygodnie. Inna reprezentacja, rozpowszechniona dzisiaj, nazywa się uzupełnieniową. Polega ona na traktowaniu pierwszej od lewej pozycji jako \(-2^{k-1}\). Tak więc zapis składający się z bitów \(b_{k-1}b_{k-2}\ldots b_2b_1b_0\) reprezentuje liczbę całkowitą

\(b_0 \cdot 2^0 + b_1 \cdot 2^1 + b_2 \cdot 2^2 + \ldots + b_{k-2} \cdot 2^{k-2} - b_{k-1} \cdot 2^{k-1}\).

Liczba całkowita w reprezentacji uzupełnieniowej należy do zakresu od \(-2^{k-1}\) (zapis: jedynka i same zera) do \(2^{k-1}-1\) (zapis: zero i same jedynki). Np. dla 16-bitowego typu short int zakresem jest \([-32\,768,32\,767]\), a dla 32-bitowego typu long int: \([-2\,147\,483\,648,2\,147\,483\,647]\).

W reprezentacji uzupełnieniowej dodawanie i odejmowanie wykonuje się tak jak dla liczb nieujemnych, tylko zapominamy o ostatnim przeniesieniu. Na przykład dla \(k=8\) wykonajmy dodawanie \(-64+64\):

 -128   64   32   16   8   4   2   1   wartość bitu 
 7  6  5  4  3  2  1  0  numer bitu
 1  1  0  0  0  0  0  0  (-64)
 +   0  1  0  0  0  0  0  0  (64)
 0  0  0  0  0  0  0  0  (0)

Natomiast dodawanie \(-64+(-128)\) będzie wyglądało następująco:

 -128   64   32   16   8   4   2   1   wartość bitu 
 7  6  5  4  3  2  1  0  numer bitu
 1  1  0  0  0  0  0  0  (-64)
 +   1  0  0  0  0  0  0  0  (-128)
 0  1  0  0  0  0  0  0  (64)

Zauważmy, że w ostatnim przypadku wynik jest błędny. Jest tak dlatego, że faktyczny wynik -192 nie mieści się w 8-bitowej reprezentacji.

Część techniczna: Błędy kompilacji

Wiemy już, że kompilator języka C++ jest pod wieloma względami dosyć restrykcyjny. Drobne usterki, takie jak literówka czy "czeski błąd", zazwyczaj od razu powodują, że kompilator zgłosi błąd i zaprzestanie próby kompilowania programu. Taką sytuację określamy mianem błędu kompilacji. Radzenie sobie z takimi błędami jest zazwyczaj całkiem proste – wystarczy przeczytać dokładnie treść błędu zgłoszonego przez kompilator i poprawić odpowiedni fragment kodu. Jeśli kompilacja nie udała się, Code Blocks zaznacza wiersz, który spowodował błąd kompilacji, a na dole ekranu wyświetla opis błędu.


Zobacz tekst nagrania

Jest też kilka innych typów błędów, na jakie może natknąć się programista. Gdy program skompiluje się poprawnie, może dawać w wyniku błędne odpowiedzi, czyli działać inaczej, niż miał w zamierzeniu. Może też np. zakończyć się błędem wykonania (patrz przykład z dzieleniem przez 0). Sposoby radzenia sobie z poszczególnymi typami błędów będziemy przedstawiać w częściach technicznych kolejnych lekcji.

Zadania

W tej lekcji mamy dla Ciebie trzy zadania. W każdym zadaniu, w sekcji "Wejście" znajduje się opis danych, które program ma wczytać, natomiast w sekcji "Wyjście" podano wymagany sposób wypisania wyniku. Twoje rozwiązanie zostanie sprawdzone automatycznie z użyciem pewnej liczby testów, czyli różnych zestawów danych wejściowych (możesz to sobie wyobrazić tak, że nasz automat uruchamia Twój program, "wpisuje" odpowiednie dane i sprawdza wynik jego działania). W każdym teście dane wejściowe są idealnie zgodne z opisem podanym w sekcji "Wejście", a wynik Twojego programu musi dokładnie odpowiadać temu, co jest opisane w sekcji "Wyjście". Jak wspominaliśmy poprzednio, dopuszczalne są nadmiarowe spacje na końcach wierszy i puste wiersze na końcu wyjścia. Program nie może wypisywać żadnych dodatkowych komunikatów, o które nie jest proszony w treści zadania. Twój program zostanie uznany za poprawny, jeśli zadziała poprawnie na wszystkich naszych testach.

Przypominamy, że zadania należy rozwiązywać samodzielnie. Jeśli chcesz, możesz przy rozwiązywaniu wyszukiwać dodatkowe informacje w Internecie lub zasięgnąć pomocy u nauczyciela lub konsultanta. Zabronione jest jednak wspólne rozwiązywanie zadań przez uczestników kursu lub proszenie innych o napisanie rozwiązań za nas.

A oto obiecane zadania – powodzenia!

Zadania przypisane do tej lekcji:
2. Prostopadłościan
3. Czas
1. Na odwrót